dotnet / dotnet/aspnetcore

Design Proposal: Structured Logging for ComponentState Transitions

Open
#65,741 0 comments 0 reactions 0 assignees View on GitHub
area-blazor design-proposal
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.