dotnet / dotnet/aspnetcore

[Validation] Custom event registration rejects name collision with browser events

Open
#69,104 2 comments 1 reaction 1 assignee Claimed by @BharatRamsf3693 View on GitHub
area-blazor Validation validation-scenario
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

**Scenario contact:** @dariatiurina

## Scenario

**This scenario validates an upgrade-breaking behavioral change.** Blazor custom events let developers wrap a browser event, extract custom arguments, and fire a .NET callback. The API is `Blazor.registerCustomEventType(eventName, { browserEventName: 'scroll',... })`.

Previously, nothing stopped a developer from passing the same name for both the custom event and the browser event it wraps:

```javascript
Blazor.registerCustomEventType('scroll', { browserEventName: 'scroll' });
```

This compiled and ran, but the event fired **twice** for every user action: once for the native browser event and once for the custom wrapper that re-dispatched the same name. The double invocation was a silent bug that led to confusing state mutations and duplicate side effects.

Starting in .NET 11, `Blazor.registerCustomEventType` validates that the custom event name differs from `browserEventName` and throws synchronously when they match. Code that relied on the double-firing (intentionally or not) will break at startup rather than run incorrectly.

## Minimum build

.NET 11 RC1

## Configurations to cover

* Blazor Web App
* [ ] Static SSR
* [x] Interactive Server
* [x] Interactive WebAssembly
* [ ] Interactive Auto
* [x] Standalone WebAssembly
* [ ] Hybrid (MAUI)

The validation runs in the browser JavaScript, so any interactive mode exercises the same code path. Static SSR does not run client-side JavaScript.

## Also exercise

* [ ] Published output
* [x] An existing .NET 10 app upgraded to .NET 11
* [ ] Trimming or ahead-of-time compilation
* [ ] More than one server instance, or a proxy in front
* [ ] Hot Reload
* [ ] An IDE as well as the command line
* [ ] Container

## Setup

Register the custom event from a JavaScript initializer, which is the documented way to do it. The file must live in `wwwroot` and be named after the app's package ID or assembly name:

`wwwroot/{PACKAGE ID/ASSEMBLY NAME}.lib.module.js`

For a Blazor Web App, register in `afterWebStarted`:

```javascript
function eventArgsCreator(event) {
return { detail: event.srcElement?.id };
}

export function afterWebStarted(blazor) {
// The case under test: the custom event name is the same as the browser event it wraps.
blazor.registerCustomEventType('scroll', {
browserEventName: 'scroll',
createEventArgs: eventArgsCreator
});
}
```

For a standalone WebAssembly app the callback is `afterStarted` instead, with the same body. Register each event type only once.

The corrected form needs a C# declaration for the custom event, otherwise the page will not compile. The declaration has requirements that are easy to miss: it has to be in a `.cs` file rather than a `.razor` file, the class must be `public` and named exactly `EventHandlers` for the Razor compiler to find it.

```csharp
using Microsoft.AspNetCore.Components;

namespace MyApp.CustomEvents;

public class CustomScrollEventArgs : EventArgs
{
public string? Detail { get; set; }
}

[EventHandler("oncustomscroll", typeof(CustomScrollEventArgs),
enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
}
```

Only the custom event needs this declaration. The native `@onscroll` handler that the test page uses as its comparison counter is already declared by the framework, so do not declare `onscroll` yourself.

## What to build

A Blazor app that registers a custom event with the same name as its underlying browser event. Observe that the call now throws at registration time rather than producing double-invocation at runtime.

Give the test element both the native handler and the custom handler, each with its own counter, so a single user action shows how many times each fired.

## Things to try

* Start with .NET 10 and observe the behavior. Then update to .NET 11 and observe again to confirm the fix.
* Register a custom event where the name equals the browser event name, such as `Blazor.registerCustomEventType('scroll', { browserEventName: 'scroll' })`. The registration call should throw.
* Observe the error message in the browser console and confirm it explains the collision.
* Correct the registration by choosing a different custom event name (e.g., `customscroll`) and verify the event fires exactly once per user action.
* Trigger the event with a single genuine user action per measurement (e.g., a scroll move/gesture). Do not automate this.

## Expected behavior

`Blazor.registerCustomEventType` throws when the custom event name equals the `browserEventName` option. The error message names both values and tells the developer to choose a different name.

### Must hold

* Calling `Blazor.registerCustomEventType('scroll', { browserEventName: 'scroll' })` throws a JavaScript error synchronously.
* The error contains a descriptive message and names the conflicting value.
* After correcting the name to something different (e.g., `customscroll`), the registration succeeds and the event fires exactly once per user interaction.

## Evidence to capture

The error text from the browser console for the colliding registration, and the two counter readings after the same number of user actions on .NET 10 and on .NET 11.

## Documentation to use

* [Custom event arguments in Blazor](https://learn.microsoft.com/aspnet/core/blazor/components/event-handling#custom-event-arguments)
* [Breaking change: Blazor custom event registration throws when name matches a browser event](https://learn.microsoft.com/aspnet/core/breaking-changes/11/blazor-custom-event-name-collision)

## What to report

Report results using the format described in the [validation testing manual](https://github.com/dotnet/aspnetcore/issues/68479). Include link to a repository with the test app.

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.