Skip to content

fix: forward wrapped access of hidden interface members to the declaring interface - #848

Open
vbreuss wants to merge 2 commits into
mainfrom
fix/hidden-property-wrapping
Open

fix: forward wrapped access of hidden interface members to the declaring interface#848
vbreuss wants to merge 2 commits into
mainfrom
fix/hidden-property-wrapping

Conversation

@vbreuss

@vbreuss vbreuss commented Aug 25, 2026

Copy link
Copy Markdown
Member

A property, indexer or event that hides a base interface member via new is emitted as an explicit interface implementation, but those implementations emitted no wrapping branch at all. On a mock created with .Wrapping(instance) the wrapped instance was never consulted for the hidden member: getters returned the mock default, setters only reached the registry, and event subscriptions never arrived at the instance.

Let the explicit implementations take the wrapping branch and cast MockRegistry.Wraps to ExplicitImplementation when set, mirroring the method fix in #847. Casting to the mocked type instead would not compile, because the hiding member has a different type (CS0266, or CS0029 when the types are unrelated).

Init-only setters stay unforwarded, since the wrapped instance is already constructed.

…ing interface

A property, indexer or event that hides a base interface member via `new` is emitted as an explicit interface implementation, but those implementations emitted no wrapping branch at all. On a mock created with `.Wrapping(instance)` the wrapped instance was never consulted for the hidden member: getters returned the mock default, setters only reached the registry, and event subscriptions never arrived at the instance.

Let the explicit implementations take the wrapping branch and cast `MockRegistry.Wraps` to `ExplicitImplementation` when set, mirroring the method fix in #847. Casting to the mocked type instead would not compile, because the hiding member has a different type (CS0266, or CS0029 when the types are unrelated).

Init-only setters stay unforwarded, since the wrapped instance is already constructed.
@vbreuss vbreuss self-assigned this Aug 25, 2026
Copilot AI lite review requested due to automatic review settings August 25, 2026 18:55
@vbreuss vbreuss added the bug Something isn't working label Aug 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Mockolate’s wrapping delegation for interface members (properties, indexers, events) that hide base interface members via new and therefore must be emitted as explicit interface implementations. It updates the source generator so explicit implementations take the wrapping branch by casting MockRegistry.Wraps to the declaring interface (matching the earlier method fix in #847), and adds regression tests + documentation to cover the behavior.

Changes:

  • Update the source generator to forward wrapped access for explicitly implemented hidden properties/indexers/events by casting Wraps to ExplicitImplementation when present.
  • Add runtime and generator-level regression tests for hidden member wrapping across differing member types (including unrelated types and init-only behavior).
  • Document wrapping semantics for hidden members and init-only setters.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Tests/Mockolate.Tests/TestHelpers/IChocolateShelf.cs Adds an interface hierarchy with new-hidden members to exercise explicit-implementation wrapping behavior.
Tests/Mockolate.Tests/MockTests.WrappingInterfaceTests.cs Adds runtime regression tests verifying wrapped delegation/subscription goes to the declaring interface for hidden members.
Tests/Mockolate.SourceGenerators.Tests/MockTests.cs Adds generator snapshot assertions ensuring emitted code casts Wraps to the declaring interface for hidden members.
Tests/Mockolate.ExampleTests/TestData/IUserCache.cs Adds an example interface hierarchy demonstrating hidden member wrapping.
Tests/Mockolate.ExampleTests/ExampleTests.cs Adds an example test showing hidden-member forwarding on wrapped instances.
Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs Implements the core generator fix: use ExplicitImplementation ?? className as the Wraps cast target for hidden members.
Docs/pages/01-create-mocks.md Documents hidden-member wrapping semantics and init-only setter behavior under wrapping.
Suppressed comments (1)

Tests/Mockolate.ExampleTests/ExampleTests.cs:305

  • If you rename the base interface (e.g. to IUserCacheBase), update the explicit interface implementation on the wrapped instance to match, otherwise this example won’t compile.
		IEnumerable<User> IReadOnlyUserCache.Users
		{
			get => ReadOnlyUsers;
			set => ReadOnlyUsers = value;
		}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +5 to +13
public interface IReadOnlyUserCache
{
IEnumerable<User> Users { get; set; }
}

public interface IUserCache : IReadOnlyUserCache
{
new IList<User> Users { get; set; }
}
Comment on lines +279 to +284
sut.Users = [alice,];
((IReadOnlyUserCache)sut).Users = [bob,];

// Each interface sees its own member on the wrapped instance.
await That(sut.Users).IsEqualTo([alice,]);
await That(((IReadOnlyUserCache)sut).Users).IsEqualTo([bob,]);
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Test Results

    24 files  ± 0      24 suites  ±0   12m 20s ⏱️ +57s
 4 366 tests +15   4 364 ✅ +15  2 💤 ±0  0 ❌ ±0 
27 738 runs  +73  27 734 ✅ +73  4 💤 ±0  0 ❌ ±0 

Results for commit 00aff0c. ± Comparison against base commit c60eb7f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.62GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Event Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 325.3 ns 19.56 ns 18.30 ns 1.02 1.7 KB 1.00
Mockolate 317.6 ns 18.42 ns 17.23 ns 1.00 1.7 KB 1.00
Imposter 1,438.2 ns 46.60 ns 43.59 ns 4.54 8.8 KB 5.17
TUnitMocks 186.5 ns 1.80 ns 1.59 ns 0.59 1.34 KB 0.79
Moq 16,528.3 ns 241.57 ns 225.96 ns 52.19 12.51 KB 7.34
NSubstitute 6,209.3 ns 67.09 ns 62.76 ns 19.61 9.05 KB 5.31
FakeItEasy 216,755.4 ns 370.43 ns 346.50 ns 684.46 15.26 KB 8.96
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 196.4 ns 4.54 ns 4.24 ns 0.52 1.93 KB 1.00
Mockolate 1 377.1 ns 4.28 ns 4.01 ns 1.00 1.93 KB 1.00
Imposter 1 523.2 ns 3.23 ns 2.86 ns 1.39 4.04 KB 2.09
TUnitMocks 1 503.9 ns 4.76 ns 4.22 ns 1.34 2.02 KB 1.04
Moq 1 131,526.0 ns 573.50 ns 508.39 ns 348.85 14.59 KB 7.56
NSubstitute 1 5,530.0 ns 25.57 ns 21.35 ns 14.67 9.12 KB 4.72
FakeItEasy 1 5,328.5 ns 82.09 ns 68.55 ns 14.13 8.05 KB 4.17
baseline* 10 465.5 ns 4.77 ns 4.46 ns 0.73 2.14 KB 1.00
Mockolate 10 638.6 ns 3.57 ns 3.34 ns 1.00 2.14 KB 1.00
Imposter 10 1,101.5 ns 6.79 ns 6.02 ns 1.72 5.52 KB 2.58
TUnitMocks 10 1,435.6 ns 7.06 ns 6.60 ns 2.25 3.73 KB 1.74
Moq 10 134,191.4 ns 396.90 ns 371.26 ns 210.14 18.48 KB 8.63
NSubstitute 10 8,202.5 ns 42.35 ns 35.36 ns 12.84 12.07 KB 5.64
FakeItEasy 10 8,665.1 ns 93.39 ns 82.79 ns 13.57 15.42 KB 7.20
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
INTEL XEON PLATINUM 8573C 2.30GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Indexer N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 1,020.8 ns 11.87 ns 10.52 ns 1.22 3.77 KB 1.00
Mockolate 1 836.6 ns 20.73 ns 19.39 ns 1.00 3.77 KB 1.00
Imposter 1 737.5 ns 13.67 ns 12.12 ns 0.88 5.16 KB 1.37
Moq 1 97,229.7 ns 1,705.43 ns 1,595.26 ns 116.28 20.6 KB 5.47
NSubstitute 1 9,057.6 ns 139.06 ns 123.27 ns 10.83 12.78 KB 3.39
FakeItEasy 1 7,210.9 ns 308.56 ns 288.63 ns 8.62 13.62 KB 3.62
baseline* 10 2,660.4 ns 15.15 ns 13.43 ns 1.10 4.82 KB 1.00
Mockolate 10 2,414.9 ns 38.78 ns 34.38 ns 1.00 4.82 KB 1.00
Imposter 10 1,768.7 ns 26.40 ns 24.70 ns 0.73 7.97 KB 1.65
Moq 10 106,904.9 ns 1,757.57 ns 1,644.03 ns 44.28 30.11 KB 6.25
NSubstitute 10 21,046.6 ns 215.93 ns 191.41 ns 8.72 25.58 KB 5.31
FakeItEasy 10 17,311.8 ns 392.28 ns 366.94 ns 7.17 32.98 KB 6.84
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Callback Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 327.5 ns 6.35 ns 5.30 ns 0.97 1.57 KB 1.00
Mockolate 337.0 ns 8.46 ns 7.50 ns 1.00 1.57 KB 1.00
Imposter 457.6 ns 13.20 ns 12.35 ns 1.36 2.38 KB 1.52
TUnitMocks 560.6 ns 18.85 ns 16.71 ns 1.66 1.99 KB 1.27
Moq 100,030.3 ns 530.13 ns 469.94 ns 296.94 8.96 KB 5.71
NSubstitute 4,995.7 ns 38.79 ns 32.39 ns 14.83 7.71 KB 4.91
FakeItEasy 5,101.6 ns 83.93 ns 78.51 ns 15.14 6.81 KB 4.33
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
INTEL XEON PLATINUM 8573C 3.00GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Property N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 441.1 ns 8.17 ns 7.24 ns 0.80 2.41 KB 1.00
Mockolate 1 549.4 ns 6.37 ns 5.96 ns 1.00 2.41 KB 1.00
Imposter 1 412.4 ns 14.69 ns 13.74 ns 0.75 3.13 KB 1.29
TUnitMocks 1 465.2 ns 5.46 ns 5.11 ns 0.85 1.64 KB 0.68
Moq 1 8,742.5 ns 89.19 ns 79.07 ns 15.92 10.39 KB 4.30
NSubstitute 1 6,063.0 ns 123.67 ns 115.68 ns 11.04 11.45 KB 4.74
FakeItEasy 1 5,543.3 ns 80.71 ns 71.55 ns 10.09 11.23 KB 4.65
baseline* 10 791.5 ns 7.13 ns 5.96 ns 0.62 2.91 KB 1.00
Mockolate 10 1,269.5 ns 37.38 ns 34.96 ns 1.00 2.91 KB 1.00
Imposter 10 1,210.0 ns 39.27 ns 34.82 ns 0.95 4.67 KB 1.61
TUnitMocks 10 1,930.9 ns 33.76 ns 29.93 ns 1.52 3.94 KB 1.35
Moq 10 15,886.0 ns 228.02 ns 213.29 ns 12.52 18.28 KB 6.29
NSubstitute 10 14,664.0 ns 300.73 ns 281.30 ns 11.56 21.08 KB 7.25
FakeItEasy 10 15,166.7 ns 310.03 ns 290.01 ns 11.96 30.8 KB 10.60
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

CreateMock Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 9.815 ns 0.1868 ns 0.1747 ns 0.57 160 B 1.00
Mockolate 17.16 ns 0.178 ns 0.158 ns 1.00 160 B 1.00
Imposter 281.31 ns 2.454 ns 2.295 ns 16.40 2248 B 14.05
TUnitMocks 34.00 ns 0.160 ns 0.150 ns 1.98 200 B 1.25
Moq 1,429.34 ns 5.205 ns 4.614 ns 83.32 2096 B 13.10
NSubstitute 1,910.74 ns 7.196 ns 6.379 ns 111.38 5048 B 31.55
FakeItEasy 1,786.27 ns 16.088 ns 15.049 ns 104.13 2763 B 17.27

baseline* rows show the corresponding Mockolate benchmark from the most recent successful main branch build with results, for regression comparison.

Copilot AI review requested due to automatic review settings August 25, 2026 19:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Tests/Mockolate.ExampleTests/TestData/IUserCache.cs:8

  • IReadOnlyUserCache exposes a settable Users property, which conflicts with the interface name and makes the example harder to understand. Consider either renaming the interface to something that doesn’t imply read-only semantics (e.g., IUserCacheBase) or making the property truly read-only and adjusting the example accordingly.
public interface IReadOnlyUserCache
{
	IEnumerable<User> Users { get; set; }
}

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants