fix: forward wrapped access of hidden interface members to the declaring interface - #848
fix: forward wrapped access of hidden interface members to the declaring interface#848vbreuss wants to merge 2 commits into
Conversation
…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.
There was a problem hiding this comment.
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
WrapstoExplicitImplementationwhen 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.
| public interface IReadOnlyUserCache | ||
| { | ||
| IEnumerable<User> Users { get; set; } | ||
| } | ||
|
|
||
| public interface IUserCache : IReadOnlyUserCache | ||
| { | ||
| new IList<User> Users { get; set; } | ||
| } |
| 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,]); |
🚀 Benchmark ResultsDetails
Details
Details
Details
Details
Details
|
There was a problem hiding this comment.
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
IReadOnlyUserCacheexposes a settableUsersproperty, 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; }
}
|



A property, indexer or event that hides a base interface member via
newis 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.WrapstoExplicitImplementationwhen 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.