Skip to content

fix(mocks): unique __argArray locals per event in RaiseEvent dispatch (#5423)#5442

Merged
thomhurst merged 2 commits intomainfrom
fix/5423-duplicate-argarray-multi-events
Apr 7, 2026
Merged

fix(mocks): unique __argArray locals per event in RaiseEvent dispatch (#5423)#5442
thomhurst merged 2 commits intomainfrom
fix/5423-duplicate-argarray-multi-events

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Summary

  • Fixes [Bug]: CS0128 when interface has more than one event #5423: generated RaiseEvent switch declared __argArray once per case in the same scope, producing CS0128/CS0165 whenever an interface exposed more than one multi-parameter event (e.g. an interface inheriting both INotifyPropertyChanging and INotifyPropertyChanged).
  • Suffix the pattern variable with the event name (__argArray_<EventName>) so each case branch has its own local.

Test plan

  • Added snapshot test MockGeneratorTests.Interface_With_Multiple_Multi_Parameter_Events covering two multi-parameter delegate events on a single interface.
  • Added runtime regression EventTests.Mock_With_Multiple_Multi_Parameter_Events_Compiles_And_Raises using the exact INotifyPropertyChanging + INotifyPropertyChanged repro from the issue.
  • Both tests pass locally on net10.0.

Generated RaiseEvent switch declared __argArray inside each case branch
of the same scope, causing CS0128/CS0165 when an interface had more than
one multi-parameter event. Suffix the local with the event name so each
case has its own variable.

Fixes #5423
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Summary

This is a clean, minimal fix for a real compiler error (CS0128/CS0165) that occurred when a mocked interface had more than one multi-parameter event. The root cause is that C# switch branches share scope unless explicitly wrapped in braces, so declaring if (args is object?[] __argArray) in two separate cases produced a duplicate variable error.


The Fix — Correct, But There's a More Idiomatic Alternative

Current approach (naming the variable per event):

case "First":
    if (args is object?[] __argArray_First) { ... }
    break;
case "Second":
    if (args is object?[] __argArray_Second) { ... }
    break;

This works. However, a more idiomatic C# solution is to give each case its own braced block {}, which creates a new scope and lets all cases reuse the same clean name:

case "First":
{
    if (args is object?[] __argArray) { ... }
    break;
}
case "Second":
{
    if (args is object?[] __argArray) { ... }
    break;
}

Why this is preferable:

  • It's the standard C# idiom for scoping locals inside switch cases — readers immediately recognise the intent.
  • Variable names stay short and uniform (__argArray vs __argArray_PropertyChanging, __argArray_PropertyChanged, etc. for deeply inherited interfaces).
  • Eliminates any theoretical risk of the event name containing characters that are valid in an event identifier but awkward inside a generated local variable name (not a current problem since C# event names are also valid identifiers, but defensive).
  • The braced-block pattern generalises better if more locals are ever added inside a case.

The current fix is not wrong — it just encodes naming information in the variable rather than using scoping. Either approach is acceptable; the braced-block version is worth considering for a follow-up.


Test Coverage — Well Done

  • Snapshot test (): correctly exercises two distinct multi-parameter delegate types on a single interface, and the file is committed (not ). The snapshot output clearly shows / in the generated switch.
  • Runtime regression (): uses the exact repro from the issue ( + ), verifies both events dispatch independently, and subscribes/raises/asserts in one readable test. This is exactly the right level of coverage for a regression fix.

Minor Observation

The interface definition placed in :

public interface IMultiEventNotifier : INotifyPropertyChanging, INotifyPropertyChanged { }

is a top-level public type in a test file. Consider making it internal or nesting it inside the test class — it's not part of the public API and the public visibility may leak into the assembly's public surface unnecessarily. Minor point, not a blocker.


Verdict

The bug is real, the fix is correct, and the test coverage is solid. The braced-block approach is worth considering as a follow-up refactor for cleaner generated code, but this PR is good to merge as-is.

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Summary

This is a clean, minimal fix for a real compiler error (CS0128/CS0165) that occurred when a mocked interface had more than one multi-parameter event. The root cause is that C# switch case branches share scope unless explicitly wrapped in braces, so declaring if (args is object?[] __argArray) in two separate cases produced a duplicate variable error.


The Fix — Correct, But There Is a More Idiomatic Alternative

The current approach names the pattern variable per event (__argArray_First, __argArray_Second). This works, but a more idiomatic C# solution is to give each case its own braced block {}, which creates a new scope and allows all cases to reuse the same clean name __argArray.

Why the braced-block approach is worth considering:

  • It is the standard C# idiom for scoping locals inside switch cases.
  • Variable names stay short and uniform — for deeply inherited interfaces the suffixed names can get verbose (e.g. __argArray_PropertyChanging, __argArray_PropertyChanged).
  • Generalises better if more locals are added inside a case in the future.

The current fix is not wrong — it is a valid minimal change. The braced-block alternative is worth considering as a follow-up refactor in MockImplBuilder.cs.


Test Coverage — Well Done

  • Snapshot test (Interface_With_Multiple_Multi_Parameter_Events): exercises two distinct multi-parameter delegate types on a single interface. The .verified.txt file is correctly committed (not .received.txt), and the snapshot clearly shows the fixed variable names in the generated switch.
  • Runtime regression (Mock_With_Multiple_Multi_Parameter_Events_Compiles_And_Raises): uses the exact repro from the issue (INotifyPropertyChanging + INotifyPropertyChanged), verifies both events dispatch independently, and is written in a clear subscribe/raise/assert style. Exactly the right coverage for a regression fix.

Minor Observation

The interface definition added in EventTests.cs:

public interface IMultiEventNotifier : INotifyPropertyChanging, INotifyPropertyChanged { }

is a top-level public type inside a test file. Consider making it internal — it is not part of the library's public API and the public modifier leaks it into the assembly's public surface unnecessarily. Minor point, not a blocker.


Verdict

The bug is real, the fix is correct, and the test coverage is solid. The braced-block approach is worth a follow-up for cleaner generated code, but this PR is good to merge as-is.

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 7, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

Address review feedback: instead of suffixing the pattern variable with
the event name, wrap each case in `{ }` so locals get their own scope.
Cleaner generated code, allows reuse of the short `__argArray` name, and
generalises if more locals are added per case.

Updates affected snapshots.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review

Previous Feedback Addressed

The main suggestion from the prior reviews — using braced blocks per case instead of event-name-suffixed variable names — has been implemented in commit 6e9eb115. The generated RaiseEvent switch now looks like:

case "First":
{
    if (args is object?[] __argArray) { ... }
    break;
}
case "Second":
{
    if (args is object?[] __argArray) { ... }
    break;
}

This is the idiomatic C# approach and is clearly the better outcome. Good follow-through on the review feedback.


One Item Still Open

The IMultiEventNotifier interface in EventTests.cs remains public:

public interface IMultiEventNotifier : INotifyPropertyChanging, INotifyPropertyChanged { }

The prior review flagged this as a minor point — it is a test-only type leaking into the assembly's public surface. Changing it to internal would be cleaner, though the mock generator requires the type to be visible enough that it can be referenced by the generated extension types. Worth confirming whether internal is sufficient here (it likely is, since the generated code lives in the same assembly in tests), but this is not a blocker.


Stale PR Description

The PR description still says "Suffix the pattern variable with the event name" but the implementation now uses braced scopes instead. Minor, but worth updating the description to reflect the actual implementation.


Verdict

Both commits together produce a correct, idiomatic fix with solid test coverage. Ready to merge.

intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Apr 8, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.28.7 to
1.29.0.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.29.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.29.0 -->

## What's Changed
### Other Changes
* 🤖 Update Mock Benchmark Results by @​thomhurst in
thomhurst/TUnit#5420
* fix(mocks): resolve build errors when mocking Azure SDK clients by
@​thomhurst in thomhurst/TUnit#5440
* fix: deduplicate virtual hook overrides across class hierarchy
(#​5428) by @​thomhurst in thomhurst/TUnit#5441
* fix(mocks): unique __argArray locals per event in RaiseEvent dispatch
(#​5423) by @​thomhurst in thomhurst/TUnit#5442
* refactor(mocks): extract MockTypeModel.Visibility helper by
@​thomhurst in thomhurst/TUnit#5443
* fix(mocks): preserve nullable annotations on generated event
implementations by @​thomhurst in
thomhurst/TUnit#5444
* fix(mocks): preserve nullability on event handler types (#​5425) by
@​thomhurst in thomhurst/TUnit#5445
### Dependencies
* chore(deps): update tunit to 1.28.7 by @​thomhurst in
thomhurst/TUnit#5416
* chore(deps): update dependency polyfill to v10 by @​thomhurst in
thomhurst/TUnit#5417
* chore(deps): update dependency polyfill to v10 by @​thomhurst in
thomhurst/TUnit#5418
* chore(deps): update dependency mockolate to 2.4.0 by @​thomhurst in
thomhurst/TUnit#5431
* chore(deps): update mstest to 4.2.1 by @​thomhurst in
thomhurst/TUnit#5433
* chore(deps): update dependency microsoft.net.test.sdk to 18.4.0 by
@​thomhurst in thomhurst/TUnit#5435
* chore(deps): update microsoft.testing to 2.2.1 by @​thomhurst in
thomhurst/TUnit#5432
* chore(deps): update dependency
microsoft.testing.extensions.codecoverage to 18.6.2 by @​thomhurst in
thomhurst/TUnit#5437
* chore(deps): update dependency @​docusaurus/theme-mermaid to ^3.10.0
by @​thomhurst in thomhurst/TUnit#5438
* chore(deps): update docusaurus to v3.10.0 by @​thomhurst in
thomhurst/TUnit#5439


**Full Changelog**:
thomhurst/TUnit@v1.28.7...v1.29.0

Commits viewable in [compare
view](thomhurst/TUnit@v1.28.7...v1.29.0).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.28.7&new-version=1.29.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: CS0128 when interface has more than one event

1 participant