Skip to content

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

Merged
vbreuss merged 3 commits into
mainfrom
fix/hidden-property-wrapping
Aug 26, 2026
Merged

fix: forward wrapped access of hidden interface members to the declaring interface#848
vbreuss merged 3 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 thread Tests/Mockolate.ExampleTests/TestData/IUserCache.cs Outdated
Comment thread Tests/Mockolate.ExampleTests/ExampleTests.cs Outdated
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Test Results

    24 files  ± 0      24 suites  ±0   12m 11s ⏱️ +48s
 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 cde9408. ± 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 9V45 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-v4

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.89 1.7 KB 1.00
Mockolate 171.7 ns 1.90 ns 1.58 ns 1.00 1.7 KB 1.00
Imposter 705.5 ns 4.96 ns 4.64 ns 4.11 8.8 KB 5.17
TUnitMocks 102.4 ns 1.66 ns 1.55 ns 0.60 1.34 KB 0.79
Moq 7,508.1 ns 63.60 ns 56.38 ns 43.73 12.66 KB 7.44
NSubstitute 2,673.8 ns 28.06 ns 24.88 ns 15.57 9.05 KB 5.31
FakeItEasy 98,159.4 ns 807.98 ns 755.79 ns 571.68 15.27 KB 8.97
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.53 1.93 KB 1.00
Mockolate 1 370.6 ns 4.48 ns 3.97 ns 1.00 1.93 KB 1.00
Imposter 1 539.3 ns 6.95 ns 6.50 ns 1.46 4.04 KB 2.09
TUnitMocks 1 518.5 ns 5.34 ns 4.99 ns 1.40 2.02 KB 1.04
Moq 1 133,937.7 ns 786.25 ns 696.99 ns 361.47 14.59 KB 7.56
NSubstitute 1 5,725.0 ns 32.68 ns 30.57 ns 15.45 9.12 KB 4.72
FakeItEasy 1 5,479.2 ns 39.20 ns 36.67 ns 14.79 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 639.7 ns 3.73 ns 3.49 ns 1.00 2.14 KB 1.00
Imposter 10 1,125.4 ns 13.76 ns 12.87 ns 1.76 5.52 KB 2.58
TUnitMocks 10 1,463.0 ns 13.21 ns 12.36 ns 2.29 3.73 KB 1.74
Moq 10 136,814.2 ns 977.62 ns 914.47 ns 213.88 18.48 KB 8.63
NSubstitute 10 8,601.8 ns 74.54 ns 66.08 ns 13.45 12.07 KB 5.64
FakeItEasy 10 8,774.7 ns 129.93 ns 121.54 ns 13.72 15.42 KB 7.20
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

Indexer N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 1,020.8 ns 11.87 ns 10.52 ns 1.09 3.77 KB 1.00
Mockolate 1 937.3 ns 27.57 ns 24.44 ns 1.00 3.77 KB 1.00
Imposter 1 841.6 ns 6.91 ns 6.12 ns 0.90 5.16 KB 1.37
Moq 1 220,683.0 ns 1,517.12 ns 1,344.89 ns 235.59 20.37 KB 5.41
NSubstitute 1 10,349.5 ns 43.21 ns 40.42 ns 11.05 12.84 KB 3.41
FakeItEasy 1 11,928.4 ns 44.23 ns 39.21 ns 12.73 13.63 KB 3.62
baseline* 10 2,660.4 ns 15.15 ns 13.43 ns 1.07 4.82 KB 1.00
Mockolate 10 2,490.7 ns 7.29 ns 6.46 ns 1.00 4.82 KB 1.00
Imposter 10 2,029.2 ns 11.25 ns 9.97 ns 0.81 7.97 KB 1.65
Moq 10 231,501.4 ns 1,657.21 ns 1,550.15 ns 92.95 29.89 KB 6.20
NSubstitute 10 25,030.4 ns 82.68 ns 69.04 ns 10.05 25.63 KB 5.32
FakeItEasy 10 24,760.1 ns 99.27 ns 88.00 ns 9.94 32.97 KB 6.84
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

Callback Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 327.5 ns 6.35 ns 5.30 ns 1.05 1.57 KB 1.00
Mockolate 312.0 ns 3.76 ns 3.33 ns 1.00 1.57 KB 1.00
Imposter 416.7 ns 3.41 ns 3.19 ns 1.34 2.38 KB 1.52
TUnitMocks 534.9 ns 4.24 ns 3.76 ns 1.71 1.99 KB 1.27
Moq 70,382.4 ns 534.75 ns 474.04 ns 225.60 8.88 KB 5.66
NSubstitute 4,533.7 ns 53.98 ns 50.49 ns 14.53 7.71 KB 4.91
FakeItEasy 4,480.3 ns 32.62 ns 30.51 ns 14.36 6.81 KB 4.33
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.87GHz, 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

Property N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 441.1 ns 8.17 ns 7.24 ns 0.78 2.41 KB 1.00
Mockolate 1 568.5 ns 12.89 ns 12.06 ns 1.00 2.41 KB 1.00
Imposter 1 444.5 ns 12.51 ns 11.70 ns 0.78 3.13 KB 1.29
TUnitMocks 1 480.8 ns 2.56 ns 2.40 ns 0.85 1.64 KB 0.68
Moq 1 10,894.2 ns 65.86 ns 61.61 ns 19.17 10.5 KB 4.35
NSubstitute 1 7,471.1 ns 55.43 ns 51.85 ns 13.15 11.45 KB 4.74
FakeItEasy 1 7,677.6 ns 143.33 ns 134.08 ns 13.51 11.24 KB 4.66
baseline* 10 791.5 ns 7.13 ns 5.96 ns 0.74 2.91 KB 1.00
Mockolate 10 1,074.1 ns 17.77 ns 14.84 ns 1.00 2.91 KB 1.00
Imposter 10 1,092.0 ns 12.96 ns 12.13 ns 1.02 4.67 KB 1.61
TUnitMocks 10 1,638.4 ns 16.86 ns 15.77 ns 1.53 3.94 KB 1.35
Moq 10 17,245.2 ns 147.26 ns 137.75 ns 16.06 18.39 KB 6.33
NSubstitute 10 16,597.0 ns 111.42 ns 98.77 ns 15.45 21.08 KB 7.25
FakeItEasy 10 18,278.2 ns 134.85 ns 126.14 ns 17.02 30.81 KB 10.60
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
INTEL XEON PLATINUM 8573C 3.47GHz, 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

CreateMock Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 9.815 ns 0.1868 ns 0.1747 ns 0.66 160 B 1.00
Mockolate 14.85 ns 0.250 ns 0.233 ns 1.00 160 B 1.00
Imposter 224.23 ns 1.930 ns 1.711 ns 15.10 2248 B 14.05
TUnitMocks 30.97 ns 0.782 ns 0.731 ns 2.09 200 B 1.25
Moq 1,013.95 ns 13.644 ns 12.763 ns 68.30 2096 B 13.10
NSubstitute 1,535.50 ns 32.838 ns 29.110 ns 103.43 5048 B 31.55
FakeItEasy 1,199.56 ns 29.837 ns 27.910 ns 80.80 2759 B 17.24

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; }
}

Copilot AI review requested due to automatic review settings August 26, 2026 18:30
The base interface exposes a settable Users property, so the read-only name was misleading in the living documentation. Rename the backing property accordingly.

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.

@sonarqubecloud

Copy link
Copy Markdown

@vbreuss
vbreuss merged commit cb55194 into main Aug 26, 2026
18 checks passed
@vbreuss
vbreuss deleted the fix/hidden-property-wrapping branch August 26, 2026 18:38
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
…nterface members to the declaring interface (#848) by Valentin Breuß
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
…nterface members to the declaring interface (#848) by Valentin Breuß
@github-actions

Copy link
Copy Markdown

This is addressed in release v3.4.1.

@github-actions github-actions Bot added the state: released The issue is released label Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working state: released The issue is released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants