Design Proposal: Structured Logging for ComponentState Transitions
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 276
Description
## Summary
Add structured logging to `ComponentState` for state transitions that are currently invisible — skipped cascading updates, single-delivery parameter removal, and renders skipped due to disposal — by reusing the `ILogger` already present in `Renderer`.
Design proposal for https://github.com/dotnet/aspnetcore/issues/51844
---
## Motivation and goals
In complex Blazor apps, certain `ComponentState` transitions happen silently and are extremely hard to troubleshoot without a debugger:
- A disposed component silently skips cascading parameter updates in `NotifyCascadingValueChanged` (early `return`).
- A component stops receiving single-delivery cascading parameters after first delivery — no signal is emitted.
- Renders are silently skipped in `RenderIntoBatch` when a component is disposed mid-queue.
This was [raised in a team discussion](https://github.com/dotnet/aspnetcore/issues/51844): *"In complex apps it's hard to detect state transitions that might happen during a brief period of time and cause issues."* Also explicitly noted the framework can now afford "an extra `if`" and that log calls can be linked out for production builds if needed.
The goal is to make these transitions observable via the standard `ILogger` infrastructure — no new APIs, no new tools required.
---
## In scope
1. Expose `_logger` from `Renderer` as an `internal` property — the same pattern already used for `ComponentMetrics`:
```csharp
// Existing pattern:
internal ComponentsMetrics? ComponentMetrics => _componentsMetrics;
// Add alongside it:
internal ILogger Logger => _logger;
```
2. Add new methods to `Renderer.Log.cs` — the separate file where `internal static partial class Log` is defined. Each method follows the two-part pattern already established in that file: a private `[LoggerMessage]`-generated method with `SkipEnabledCheck = true`, wrapped by a public method with a manual `IsEnabled` guard:
```csharp
// Private: source-generated. SkipEnabledCheck = true — no automatic IsEnabled check inside.
[LoggerMessage(7, LogLevel.Debug,
"Skipping cascading parameter update for component {ComponentId} ({ComponentType}): component was already disposed",
EventName = "SkippingCascadingUpdateOnDisposedComponent", SkipEnabledCheck = true)]
private static partial void SkippingCascadingUpdateOnDisposedComponent(ILogger logger, int componentId, string? componentType);
// Public wrapper: checks IsEnabled before evaluating arguments like GetType().FullName.
public static void SkippingCascadingUpdateOnDisposedComponent(ILogger logger, ComponentState componentState)
{
if (logger.IsEnabled(LogLevel.Debug)) // This is almost always false, so skip the evaluations
{
SkippingCascadingUpdateOnDisposedComponent(logger, componentState.ComponentId, componentState.Component.GetType().FullName);
}
}
```
New methods with sequential EventIds (7–10):
- `Log.SkippingCascadingUpdateOnDisposedComponent` — `Debug`
- `Log.StoppedSingleDeliveryCascadingParameters` — `Debug`
- `Log.SkippingRenderOnDisposedComponent` — `Debug`
- `Log.SupplyingCombinedParameters` — `Trace` (high-frequency path, see Risks)
3. Call these methods from `ComponentState` via `_renderer.Logger`, at the following points:
- `RenderIntoBatch` — before the `return` when `_componentWasDisposed`
- `StopSupplyingSingleDeliveryCascadingParameters` — at the end of the method, after state is updated
- `NotifyCascadingValueChanged` — before the `return` when `_componentWasDisposed`
- `SupplyCombinedParameters` — before `Component.SetParametersAsync` is called
`ComponentState` already holds `_renderer` as a field, so no new dependencies are needed — all calls use `_renderer.Logger` directly, the same way `_renderer.ComponentMetrics` is already used.
---
## Out of scope
- Visual tooling or browser DevTools integration.
- Logging the *values* of parameters (PII risk, performance cost).
- New public APIs on `ComponentState` or `Renderer`.
- Changes to `ComponentMetrics` or existing OpenTelemetry instrumentation.
---
## Risks / unknowns
- **`_logger` is `private` in `Renderer`**: `ComponentState` cannot access it directly. Exposing it as a one-line `internal` property is consistent with how `ComponentMetrics` is already exposed, but it slightly widens the internal surface of `Renderer` — worth confirming with maintainers.
- **Log verbosity on hot path**: `SupplyCombinedParameters` fires on every parameter update for every component. At `Trace` level this is high volume — developers must understand that enabling `Trace` for this category is a targeted, deliberate action. This is mitigated by design: the two-part `[LoggerMessage]` + `IsEnabled` pattern (already established in `Renderer.Log.cs`) ensures zero allocations when `Trace` is disabled.
- **Naming in `Log` class**: New method names should follow the established convention — `SkippingEventOnDisposedComponent` → `SkippingCascadingUpdateOnDisposedComponent` is a natural fit. EventIds 7–10 continue the existing sequence.
---
## Examples
A developer enables debug logging for Blazor rendering in `appsettings.Development.json`:
```json
{
"Logging": {
"LogLevel": {
"Microsoft.AspNetCore.Components.RenderTree.Renderer": "Debug"
}
}
}
```
They then see output like:
```
dbug: Microsoft.AspNetCore.Components.RenderTree.Renderer
Skipping cascading parameter update for component 42 (MyApp.Pages.Counter):
component was already disposed.
dbug: Microsoft.AspNetCore.Components.RenderTree.Renderer
Stopped supplying single-delivery cascading parameters
to component 17 (MyApp.Shared.ThemeProvider).
```
To also observe every parameter delivery (high volume), they switch to `Trace`:
```
trce: Microsoft.AspNetCore.Components.RenderTree.Renderer
Supplying combined parameters to component 42 (MyApp.Pages.Counter).
```
No code changes are required from the developer — this is purely an opt-in logging configuration. The log category `Microsoft.AspNetCore.Components.RenderTree.Renderer` already exists (line 101 in `Renderer.cs`), so no new category is introduced.
Contributor guide
Assessment
This issue has not been assessed yet.