Description
T.Mock() fails to compile with CS0205: Cannot call an abstract base member when T is an abstract class that declares an abstract indexer (this[...]).
The generated mock implementation always emits a fallback return base[index]; (or base[index] = value; for a setter) inside the indexer override, regardless of whether the indexer is abstract. That's fine for a virtual indexer with a real base implementation, but for an abstract indexer there is no base implementation to call, so the generated code doesn't compile.
Other abstract methods on the same type (e.g. GetOrdinal(string), IsDBNull(int) on System.Data.Common.DbDataReader) mock and compile without issue, so this looks specific to how indexers are handled in the generator's fallback path.
Repro steps
repro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" Version="1.62.0" />
<PackageReference Include="TUnit.Mocks" Version="1.62.0" />
</ItemGroup>
</Project>
Repro.cs:
namespace ReproTests;
public abstract class Repository
{
public abstract object this[int index] { get; }
public abstract object this[string key] { get; }
}
public class MockGenTests
{
[Test]
public async Task Mocking_AbstractClass_WithAbstractIndexer_ShouldCompile()
{
var repo = Repository.Mock();
await Task.CompletedTask;
}
}
dotnet build fails with:
error CS0205: Cannot call an abstract base member: 'Repository.this[int]'
error CS0205: Cannot call an abstract base member: 'Repository.this[string]'
Generated code (the actual bug)
With EmitCompilerGeneratedFiles enabled, the generated RepositoryMockImpl looks like this:
file sealed class RepositoryMockImpl : global::ReproTests.Repository, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
{
private readonly global::TUnit.Mocks.MockEngine<global::ReproTests.Repository> _engine;
// ...
public override object this[int index]
{
get
{
if (_engine.TryHandleCallWithReturn<object>(0, "get_Item", new object?[] { index }, default!, out var __result))
{
return __result;
}
return base[index]; // <-- CS0205: Repository.this[int] is abstract, there is no base member to call
}
}
public override object this[string key]
{
get
{
if (_engine.TryHandleCallWithReturn<object>(1, "get_Item", new object?[] { key }, default!, out var __result))
{
return __result;
}
return base[key]; // <-- CS0205: Repository.this[string] is abstract, there is no base member to call
}
}
// ...
}
For a virtual indexer, falling back to base[...] when no setup matches is reasonable. For an abstract indexer, the generator needs the same fallback strategy it already uses elsewhere for abstract methods (e.g. throw, or return default/the configured default-value-provider result) instead of unconditionally emitting a base[...] call.
Expected behavior
Repository.Mock() compiles, and calling the indexer on an unconfigured mock either:
- returns the type's default value (consistent with how unconfigured abstract methods currently behave), or
- throws an informative exception (e.g. in strict mode),
exactly as already happens for abstract non-indexer members.
Actual behavior
Build fails with CS0205 for every abstract indexer on the mocked type.
Real-world impact
This isn't just a contrived example — it blocks mocking System.Data.Common.DbDataReader (which declares public abstract object this[int ordinal] { get; } and public abstract object this[string name] { get; }), which in turn is blocking us from fully migrating a test suite off NSubstitute onto TUnit.Mocks. We've had to keep one test file on NSubstitute as a scoped, documented exception until this is fixed:
dailydevops/extensions.data#563
Environment
- TUnit / TUnit.Mocks:
1.62.0 (latest at time of filing)
- .NET SDK:
10.0.302
- Target framework:
net10.0
- OS: Windows 10.0.26200 (win-x64)
Description
T.Mock()fails to compile withCS0205: Cannot call an abstract base memberwhenTis an abstract class that declares an abstract indexer (this[...]).The generated mock implementation always emits a fallback
return base[index];(orbase[index] = value;for a setter) inside the indexer override, regardless of whether the indexer is abstract. That's fine for avirtualindexer with a real base implementation, but for anabstractindexer there is no base implementation to call, so the generated code doesn't compile.Other abstract methods on the same type (e.g.
GetOrdinal(string),IsDBNull(int)onSystem.Data.Common.DbDataReader) mock and compile without issue, so this looks specific to how indexers are handled in the generator's fallback path.Repro steps
repro.csproj:Repro.cs:dotnet buildfails with:Generated code (the actual bug)
With
EmitCompilerGeneratedFilesenabled, the generatedRepositoryMockImpllooks like this:For a
virtualindexer, falling back tobase[...]when no setup matches is reasonable. For anabstractindexer, the generator needs the same fallback strategy it already uses elsewhere for abstract methods (e.g. throw, or returndefault/the configured default-value-provider result) instead of unconditionally emitting abase[...]call.Expected behavior
Repository.Mock()compiles, and calling the indexer on an unconfigured mock either:exactly as already happens for abstract non-indexer members.
Actual behavior
Build fails with CS0205 for every abstract indexer on the mocked type.
Real-world impact
This isn't just a contrived example — it blocks mocking
System.Data.Common.DbDataReader(which declarespublic abstract object this[int ordinal] { get; }andpublic abstract object this[string name] { get; }), which in turn is blocking us from fully migrating a test suite off NSubstitute onto TUnit.Mocks. We've had to keep one test file on NSubstitute as a scoped, documented exception until this is fixed:dailydevops/extensions.data#563
Environment
1.62.0(latest at time of filing)10.0.302net10.0