Net4x.NSubstitute.Extensions
1.0.0.26243
dotnet add package Net4x.NSubstitute.Extensions --version 1.0.0.26243
NuGet\Install-Package Net4x.NSubstitute.Extensions -Version 1.0.0.26243
<PackageReference Include="Net4x.NSubstitute.Extensions" Version="1.0.0.26243" />
<PackageVersion Include="Net4x.NSubstitute.Extensions" Version="1.0.0.26243" />
<PackageReference Include="Net4x.NSubstitute.Extensions" />
paket add Net4x.NSubstitute.Extensions --version 1.0.0.26243
#r "nuget: Net4x.NSubstitute.Extensions, 1.0.0.26243"
#:package Net4x.NSubstitute.Extensions@1.0.0.26243
#addin nuget:?package=Net4x.NSubstitute.Extensions&version=1.0.0.26243
#tool nuget:?package=Net4x.NSubstitute.Extensions&version=1.0.0.26243
Net4x.NSubstitute.Extensions
Fills the gaps between NSubstitute and Moq — strict mocks, an expression-based Setup/Verify API,
MockRepository, pluggable default values, cross-substitute call order, a typed protected-member API —
and adds what neither can do on its own: concrete classes, sealed classes, non-virtual members and
static methods.
Nothing is wrapped. Everything hangs off a plain NSubstitute substitute, so there is no Mock<T> and no
.Object to unwrap: IClock.Strict() returns an IClock you hand straight to production code, and
NSubstitute's own API keeps working on it throughout.
var clock = IClock.Strict(); // strict substitute, created from the type itself
clock.Setup(x => x.Now).Returns(new DateTime(2026, 8, 30));
Assert.Equal(2026, clock.Now.Year);
clock.Verify(x => x.Now, Times.Once());
clock.VerifyNoOtherCalls(); // any other call would already have thrown
Install
dotnet add package Net4x.NSubstitute.Extensions
Pulls in NSubstitute 6.2.0 and Net4x.NSubstituteConcrete (Harmony), which powers the concrete and
static support. Import it in test projects only: the API is exposed as C# extension members, so
Setup, Verify and friends appear on every class type in scope. That is the price of the no-wrapper
design.
- Assembly:
netstandard2.0, strong-named — .NET Framework 4.x, .NET Core and .NET 5+ consumers are all covered. - Consuming projects should compile with C# 14 (
<LangVersion>latest</LangVersion>on a .NET 10 SDK): the type-first creation helpers (IClock.Strict(),Substitute.ForConcrete<T>()) are static extension members. - 235 tests, all green.
What it adds to NSubstitute
| Provided by | |
|---|---|
Strict mocks (MockBehavior.Strict) |
IFoo.Strict(), sub.AsStrict(), MockBehavior |
Expression-based Setup / Verify |
sub.Setup(…), sub.Verify(…), It, Times |
MockRepository / centralized configuration |
MockRepository |
| Typed, verifiable protected members | sub.Protected<T>(…), sub.VerifyProtected(…) |
| Pluggable default values | IDefaultValueProvider, DefaultValueProviders |
| Call order across substitutes | MockSequence |
| Concrete, sealed and non-virtual members | Substitute.ForConcrete<T>() |
| Static methods | Substitute.SetupStatic(…), Substitute.ForStatic(…) |
| Async setup sugar | ReturnsAsync, ThrowsAsync |
Everything NSubstitute already does — interfaces, virtual and abstract members, argument matching,
sequential returns, exceptions, callbacks, events, ref/out, recursive mocks, partial mocks — keeps
working unchanged, through either syntax.
Creating substitutes
var strict = IClock.Strict(); // throws on any call that was not set up
var loose = IClock.Loose(); // ordinary substitute
var partial = Account.LoosePartsOf(100m); // partial substitute, constructor args passed through
var custom = IClock.Mock(MockBehavior.Strict, DefaultValueProviders.Empty);
Substitute.For<IClock>().AsStrict(); // or adopt one NSubstitute already made
Setup
sub.Setup(x => x.Add(1, 2)).Returns(3);
sub.Setup(x => x.Add(It.IsAny<int>(), It.Is<int>(v => v > 10))).Returns(99);
sub.Setup(x => x.Add(It.IsAny<int>(), It.IsAny<int>()))
.Callback(call => log.Add(call.ArgAt<int>(0)))
.Returns(c => c.ArgAt<int>(0) + c.ArgAt<int>(1));
sub.Setup(x => x.Log("boom")).Throws<InvalidOperationException>();
sub.SetupWithAnyArgs(x => x.Add(0, 0)).Returns(42);
sub.SetupSequence(x => x.Next()).Returns(1).Throws<TimeoutException>().Returns(3);
sub.SetupGet(x => x.Name).Returns("abacus");
sub.SetupSet(x => x.Name = It.IsAny<string>());
sub.SetupProperty(x => x.Name, "initial"); // behaves like a real property
sub.Setup(x => x.Child.Describe(7)).Returns("seven"); // recursive setups
sub.Setup(x => x.LoadAsync(1)).ReturnsAsync(new User(1)); // async sugar
sub.Setup(x => x.LoadAsync(2)).ThrowsAsync(new TimeoutException());
Callback and Throws go through NSubstitute's When route while Returns is configured on the router,
so their order in the chain does not matter. SetupSequence repeats its final step once exhausted,
matching NSubstitute's multi-value Returns(a, b, c) rather than Moq's return-to-default.
Declaring a setup is not calling: it is recorded through NSubstitute's RecordCallSpecification route, so
it leaves the call record — and therefore Verify counts — untouched, and can be declared against a
strict substitute.
Verification
sub.Verify(x => x.Add(1, 2)); // at least once
sub.Verify(x => x.Add(1, 2), Times.Exactly(2));
sub.Verify(x => x.Log("x"), Times.Never(), "logging is off in this mode");
sub.VerifyWithAnyArgs(x => x.Add(0, 0), Times.Once());
sub.VerifySet(x => x.Name = "abacus", Times.Once());
sub.VerifyAll(); // every setup was used
sub.VerifyVerifiable(); // only setups marked .Verifiable()
sub.VerifyNoOtherCalls(); // nothing arrived beyond what was set up or verified
Times covers Never, Once, Exactly, AtLeast(Once), AtMost(Once) and Between with inclusive or
exclusive bounds. VerifyAll and VerifyNoOtherCalls follow substitutes reached through recursive setups.
Concrete classes, sealed classes and non-virtual members
A real instance is constructed and its members are intercepted, so nothing needs to be virtual — or even overridable. Configuration and verification use exactly the same API as any other substitute.
var service = Substitute.ForConcrete<UserService>("staging"); // the real constructor runs
// var service = UserService.Concrete("staging"); // type-first spelling
service.Setup(x => x.GetUserName(1)).Returns("John Doe");
Assert.Equal("John Doe", service.GetUserName(1));
Assert.Equal("Real User Two", service.GetUserName(2)); // unconfigured: the real code runs
service.Verify(x => x.GetUserName(1), Times.Once());
var cache = Substitute.ForConcrete<SealedCache>(); // sealed classes too
var strict = Substitute.StrictConcrete<UserService>(); // every member intercepted; unset-up calls throw
An unconfigured call runs the real implementation, so ForConcrete<T> is the concrete-class equivalent of
a partial substitute rather than of Substitute.For<T>().
Static methods
Two spellings — a direct one, and a scope that unpatches on dispose:
Substitute.SetupStatic(() => FileHelper.ReadFile(It.IsAny<string>())).Returns("mock");
Assert.Equal("mock", FileHelper.ReadFile("t.txt"));
Substitute.VerifyStatic(() => FileHelper.ReadFile("t.txt"), Times.Once());
using var files = Substitute.ForStatic(typeof(FileHelper)); // or Substitute.ForStatic<FileHelper>()
files.Setup(() => FileHelper.ReadFile("t.txt")).Returns("mock");
files.VerifyNoOtherCallsOnDispose = true;
SetupStaticWithAnyArgs, SetupStaticSequence, AllowStaticCall, VerifyStaticAll and
VerifyStaticNoOtherCalls round the direct form out; a StaticMock scope carries the same members
unprefixed, plus ResetSetups, ResetCalls, ResetAll and VerifyAllOnDispose. A scope patches only its
own type, and refuses an expression naming a static of another type.
Rules that come with runtime patching
A Harmony patch applies to a method for the whole process, not to one object, and the library is explicit about what follows from that rather than papering over it:
- Unpatch on teardown. Call
Substitute.ResetConcrete()when a fixture finishes, and disable parallelism for tests that patch (in xUnit, put them in one collection withDisableParallelization = true). AMockRepositoryor aStaticMockscope does its own share of this onDispose. - Only members a setup names are patched, unless you ask otherwise. A call to an unpatched member is
invisible, so
Times.Never()andVerifyNoOtherCallsare refused — with the flag that fixes them — rather than passing for the wrong reason. SetConcreteOptions.PatchAllMembers = true, or useMockBehavior.Strictor a default-value provider, both of which imply it. ConcreteOptions.TrackCallOrder(on by default) is what letsMockSequenceorder concrete, static and ordinary substitutes together. Turned off, a mixed sequence is refused rather than reported from two unrelated counters.Substitute.ConcreteDiagnosticsreports what is currently patched, for tracking down a leak between tests.sub.Arrange(…)is rejected on a concrete substitute: it exists so NSubstitute's own syntax can configure a strict proxy, and a concrete substitute is a real object, not a proxy. UseSetup(…).
MockRepository
using var mocks = new MockRepository(MockBehavior.Strict)
{
DefaultValueProvider = DefaultValueProviders.Empty,
VerifyAllOnDispose = true,
VerifyNoOtherCallsOnDispose = true,
};
var clock = mocks.Create<IClock>();
var repo = mocks.CreatePartial<Ledger>();
var service = mocks.CreateConcrete<UserService>("staging");
var files = mocks.CreateStatic(typeof(FileHelper));
mocks.Adopt(somethingMadeElsewhere);
VerifyAll, VerifyVerifiable and VerifyNoOtherCalls run across every substitute and report all
failures at once, each labelled with the type it stands in for. Dispose runs whichever end-of-scope checks
you enabled, resets the substitutes and closes the static scopes it opened.
Call order across substitutes
Received.InOrder checks a contiguous run within its own block. MockSequence checks an ordering that
spans independent substitutes — concrete and static ones included — using the sequence numbers NSubstitute
already stamps on every call.
MockSequence.Create()
.Expect(connection, x => x.Open())
.ExpectStatic(() => FileHelper.ReadFile("query.sql"))
.Expect(command, x => x.Execute(It.IsAny<string>()))
.Expect(connection, x => x.Close())
.Verify();
mocks.Sequence().Contiguous() // repository-wide, and no other calls in between
.Expect(log, x => x.Write("open"))
.Expect(log, x => x.Write("close"))
.Verify();
Unrelated calls between the expected ones are tolerated by default; Contiguous() forbids them. A verified
sequence counts as verification, so a later VerifyNoOtherCalls stays quiet.
Default values
Consulted before NSubstitute's own auto-values, so they can override them.
sub.WithDefaultValues(DefaultValueProviders.Empty); // "", empty collections, completed tasks
sub.WithDefaultValues(DefaultValueProviders.Substitutes); // recursive mocks for classes, not just interfaces
sub.WithDefaultValues(DefaultValueProviders.Custom()
.Register("(unset)")
.Register<int>(call => call.GetArguments().Length)
.RegisterMatching(t => t.IsEnum, (t, _) => Enum.GetValues(t).GetValue(0)));
DefaultValueProviders.Compose(first, second); // first match wins
Implement IDefaultValueProvider for anything else; returning false falls through to NSubstitute. A
strict substitute still throws rather than letting a provider quietly answer an unexpected call.
Protected members
var ledger = Ledger.LoosePartsOf();
ledger.Protected<int>("Calculate", It.IsAny<int>()).Returns(42);
ledger.ProtectedVoid("Audit", It.IsAny<string>()).Callback(c => seen.Add(c.ArgAt<string>(0)));
ledger.Protected<string>("get_Label").Returns("configured"); // properties via their accessor
ledger.VerifyProtected("Audit", Times.Once(), "posted");
ledger.AllowProtected("Audit", It.IsAny<string>()); // permit it on a strict substitute
A name that does not resolve raises NSubstitute's own ProtectedMethodNotFoundException; a non-virtual
match raises ProtectedMethodNotVirtualException; a result type that does not fit raises
UnsupportedSetupExpressionException.
Two C# limitations worth knowing
Language rules, not gaps in the library — both have a supported route around them.
1. Arg.Any<T>() cannot appear in a Setup/Verify expression. It returns ref T, and C# forbids a
by-reference return inside an expression tree (CS8153). Use It.IsAny<T>() there. Plain Arg.* keeps
working everywhere NSubstitute itself accepts it, including inside Arrange:
sub.Setup(x => x.Add(It.IsAny<int>(), 3)).Returns(7); // expression API
sub.Arrange(x => x.Add(Arg.Any<int>(), 3).Returns(7)); // native API
It also covers Is, IsIn, IsNotIn, IsInRange, IsNull, IsNotNull and IsRegex.
2. ref/out arguments cannot appear in an expression tree either. Arrange those natively. Arrange
suspends strict enforcement for its duration, so this works on a strict substitute too:
sub.Arrange(x => x.TryParse("42", out Arg.Any<int>())
.Returns(call => { call[1] = 42; return true; }));
Strict mode and existing NSubstitute code
Strictness is one custom call handler registered through NSubstitute's public
ICallRouter.RegisterCustomCallHandlerFactory, reached only when nothing earlier in the replay route
answered the call. So it is not a re-implementation of NSubstitute's matching, and:
sub.Received().Foo()andReceived.InOrderuse other routes and are never affected.- Event subscription and
object's own members never require a setup, matching Moq. sub.Foo(1).Returns(2)written directly against a strict substitute would trip the check, because the arranging call runs the replay route first. Wrap it insub.Arrange(…).- A call configured only with native
When(...).Do(...)produces no result, so a strict substitute still rejects it. Addsub.AllowCall(x => x.Foo())to permit it.
Resetting
sub.ResetSetups(); // drop setups and return values, keep the call record
sub.ResetCalls(); // drop the call record, keep setups
sub.ResetAll(); // both; behaviour is preserved
Substitute.ResetConcrete(); // drop every concrete and static patch — fixture teardown
Notes
- Per-substitute state lives in a
ConditionalWeakTable, so nothing here keeps a substitute alive. Arrange's suspension of strictness is thread-scoped; do not await inside anArrangelambda.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Net4x.NSubstituteConcrete (>= 1.0.8.26243)
- NSubstitute (>= 6.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0.26243 | 118 | 8/31/2026 |