[API Proposal] Blazor host startup values and initialization pipeline
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
## Background and Motivation
Blazor currently initializes host services through host-specific, hardcoded sequences in static SSR/prerendering, interactive Server circuits, and WebAssembly. Adding or reordering an initialization step requires coordinated changes in each host, and component libraries have no common extension point for initialization that must happen after host inputs are available but before components render.
This proposal introduces two related primitives:
1. Startup-value providers collect string values from the current `HttpContext` or browser. Hosts expose the resulting values through a common service.
2. Ordered host initializers consume those values and initialize services consistently in endpoint rendering, Server circuits, and `WebAssemblyHost`.
The initial framework consumers are the existing navigation manager, navigation interception, JS-runtime attachment, and scroll-to-location-hash initialization steps. The goal is to generalize their existing behavior rather than add a new application bootstrap model.
This follows the scenario described in #30304. The draft implementation is in #68961.
### Revision after initial approval
The original proposal injected host-scoped dependencies into each `IHostInitializer`, which required resolving and sorting scoped initializer instances for every request or circuit. The revised shape passes the active host scope to singleton initializers instead. This allows the framework to build and order one singleton initializer collection while still resolving `NavigationManager`, `IHostStartupValues`, and other scoped services from the correct request, circuit, or WebAssembly scope.
Because this changes the previously approved `InitializeAsync` signature and initializer lifetime contract, the proposal is returning to API review.
## Proposed API
```diff
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Components.Hosting;
+
+public interface IHostInitializer
+{
+ int Order { get; }
+ Task InitializeHostAsync(
+ IServiceProvider services,
+ CancellationToken cancellationToken = default);
+ Task InitializeBrowserAsync(
+ IServiceProvider services,
+ CancellationToken cancellationToken = default);
+}
+
+public interface IHostStartupValues
+{
+ string? GetValue(string key);
+ string GetRequired(string key);
+}
+
+public interface IBrowserStartupValueProvider
+{
+ IReadOnlyList Keys { get; }
+}
+
+public interface IHttpContextStartupValueProvider
+{
+ IReadOnlyDictionary GetValues(HttpContext httpContext);
+}
```
`IHostInitializer` and `IHostStartupValues` are defined in `Microsoft.AspNetCore.Components`. `IBrowserStartupValueProvider` is defined in `Microsoft.AspNetCore.Components.Web`, and `IHttpContextStartupValueProvider` in `Microsoft.AspNetCore.Components.Endpoints`; all use the `Microsoft.AspNetCore.Components.Hosting` namespace.
`Order` defaults to `0`. Both initialization methods default to returning `Task.CompletedTask`.
### Lifetime and execution contract
- `IHostInitializer` implementations are registered as singleton services.
- The `IServiceProvider` argument is the active host scope: the current HTTP request scope for SSR/prerendering, circuit scope for interactive Server, or application scope for WebAssembly.
- Initializers resolve host-scoped dependencies from that provider during initialization and must not retain the provider or resolved scoped services after the method completes.
- The framework maintains an internal singleton ordered collection. Initializer orders must be unique; duplicate orders are rejected because the framework cannot determine the intended dependency order.
- The collection creates an internal per-host invoker bound to the active service provider. The invoker owns and caches the host- and browser-initialization tasks.
- Exceptions and cancellation stop initialization and surface through the active host's existing error path.
- `InitializeHostAsync` performs work that does not require browser interop.
- `InitializeBrowserAsync` performs browser-dependent work. The invoker always awaits host initialization before invoking the browser phase.
- Static SSR invokes only the host phase. Interactive Server invokes the host phase during circuit creation and the browser phase after the SignalR startup invocation can return. WebAssembly starts both phases during `Build()` and `RunAsync` awaits the stored browser-initialization task.
- Browser keys are dot-separated JavaScript property paths resolved from `globalThis`, such as `document.baseURI` and `location.href`. Expressions are not allowed.
- Browser and HTTP values use ordinal string keys and string values. Duplicate keys are rejected.
- Initializers execute once per host activation. With prerendering, a non-JS initializer can therefore run once for SSR and again for the interactive host.
No public initializer-collection type or public host-name key is proposed.
## Usage Examples
A library can declare browser values and register singleton initializers:
```csharp
services.AddSingleton();
services.AddSingleton();
internal sealed class BrowserLocationStartupValues : IBrowserStartupValueProvider
{
public IReadOnlyList Keys { get; } =
["document.baseURI", "location.href"];
}
internal sealed class BrowserLocationInitializer : IHostInitializer
{
public int Order => -100;
public Task InitializeHostAsync(
IServiceProvider services,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var startupValues = services.GetRequiredService();
var navigationManager = services.GetRequiredService();
navigationManager.Initialize(
startupValues.GetRequired("document.baseURI"),
startupValues.GetRequired("location.href"));
return Task.CompletedTask;
}
}
```
An initializer that invokes JavaScript declares that requirement:
```csharp
internal sealed class NavigationInterceptionInitializer : IHostInitializer
{
public int Order => 100;
public Task InitializeBrowserAsync(
IServiceProvider services,
CancellationToken cancellationToken = default)
{
var navigationInterception = services.GetRequiredService();
return navigationInterception.EnableNavigationInterceptionAsync();
}
}
```
An HTTP provider remains host-scoped through the provider argument:
```csharp
services.AddSingleton();
internal sealed class RequestPathStartupValues : IHttpContextStartupValueProvider
{
public IReadOnlyDictionary GetValues(HttpContext httpContext) =>
new Dictionary
{
["request.pathBase"] = httpContext.Request.PathBase,
["request.path"] = httpContext.Request.Path,
};
}
```
## Alternative Designs
- Keep scoped initializer instances and sort `IEnumerable` for each host activation. This supports constructor injection naturally but repeats resolution and ordering for every request/circuit and prevents a singleton ordered collection.
- Use one method plus a `RequiresJSInterop` property. This forces Server to split one ordered sequence into a prefix and deferred suffix and prevents an initializer from participating in both phases.
- Add a public `HostInitializationContext` instead of passing `IServiceProvider`. This is more strongly typed but adds another public type before there are host-independent context values beyond `Services` and cancellation.
- Add separate initializers for `HttpContext`, Circuit, and `WebAssemblyHost`. This gives each initializer more host context but forces libraries to implement several host-specific contracts.
- Keep the pipeline internal. This is sufficient for framework-owned steps but does not solve the library-author scenario described in #30304.
## Risks
- Passing `IServiceProvider` is an explicit service-locator pattern. The benefit is that singleton initializer instances can operate on the correct host scope; documentation must clearly prohibit retaining the provider or scoped services.
- Requiring singleton initializer registrations is a behavioral contract that DI cannot enforce automatically when callers use raw `IServiceCollection` APIs. Resolving a scoped initializer into the singleton collection would create a captive dependency. The implementation should fail clearly under scope validation, and documentation/examples must consistently use `AddSingleton`.
- Ordering can create coupling between initializers. Duplicate orders are rejected, and framework-reserved orders need documentation.
- The two-stage Server startup sequence must run each stage at most once, preserve initializer order within each stage, and prevent component rendering from observing partially initialized services.
- Browser property paths expose browser evaluation semantics as public behavior. Invalid, dangerous, missing, duplicate, and non-string values must fail deterministically.
- Cancellation and initialization failures must flow through each host's existing error boundary without being swallowed.
Contributor guide
Research direction
Start by reviewing the proposed API and the draft implementation in #68961, then trace how initialization currently works for SSR, Server circuits, and WebAssembly. Done means the API review resolves the initializer signature, lifetime, ordering, two-stage execution, and startup-value contracts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, javascript, wasm
- Domain
- api, backend, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100