diff --git a/README.md b/README.md index cc165afcf..e4aefaa3d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# RSCG - 254 Examples of Roslyn Source Code Generators / 16 created by Microsoft / +# RSCG - 255 Examples of Roslyn Source Code Generators / 16 created by Microsoft / -The RSCG_Examples repository is a comprehensive documentation system that automatically processes and showcases 254 Roslyn Source Code Generator (RSCG) examples. The system transforms individual RSCG projects into structured documentation with code examples and cross-referenced content with a searchable website and code example exports. +The RSCG_Examples repository is a comprehensive documentation system that automatically processes and showcases 255 Roslyn Source Code Generator (RSCG) examples. The system transforms individual RSCG projects into structured documentation with code examples and cross-referenced content with a searchable website and code example exports. This system serves as both a learning resource for .NET developers interested in source generators and an automated pipeline for maintaining up-to-date documentation about the RSCG ecosystem -## Latest Update : 2026-02-02 => 02 February 2026 +## Latest Update : 2026-02-13 => 13 February 2026 If you want to see examples with code, please click ***[List V2](https://ignatandrei.github.io/RSCG_Examples/v2/docs/List-of-RSCG)*** @@ -24,8 +24,30 @@ If you want to be notified each time I add a new RSCG example , please click htt ## Content -Those are the 254 Roslyn Source Code Generators that I have tested you can see and download source code example. +Those are the 255 Roslyn Source Code Generators that I have tested you can see and download source code example. ( including 16 from Microsoft ) +### 255. [KnockOff](https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff) , in the [Tests](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#tests) category + +Generated on : 2026-02-13 => 13 February 2026 + +
+ Expand + + + +Author: Keith Voels + +A Roslyn Source Generator for creating unit test stubs. Unlike Moq's fluent runtime configuration, KnockOff uses partial classes for compile-time setup—trading flexibility for readability and performance. + +Nuget: [https://www.nuget.org/packages/KnockOff/](https://www.nuget.org/packages/KnockOff/) + + +Link: [https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff](https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff) + +Source: [https://github.com/NeatooDotNet/KnockOff](https://github.com/NeatooDotNet/KnockOff) + +
+ ### 254. [ErrorOrX](https://ignatandrei.github.io/RSCG_Examples/v2/docs/ErrorOrX) , in the [API](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#api) category Generated on : 2026-02-02 => 02 February 2026 diff --git a/later.md b/later.md index 7e1116972..6b1ece8f7 100644 --- a/later.md +++ b/later.md @@ -1,6 +1,6 @@ # Just later -## Latest Update : 2026-02-02 => 02 February 2026 +## Latest Update : 2026-02-13 => 13 February 2026 diff --git a/v2/.tours/KnockOff.tour b/v2/.tours/KnockOff.tour new file mode 100644 index 000000000..f21acfd21 --- /dev/null +++ b/v2/.tours/KnockOff.tour @@ -0,0 +1,42 @@ + +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "KnockOff", + "steps": + [ + { + "file": "rscg_examples/KnockOff/src/TestClock/TestClock.csproj", + "description": "First, we add Nuget [KnockOff](https://www.nuget.org/packages/KnockOff/) in csproj ", + "pattern": "KnockOff" + } + + ,{ + "file": "rscg_examples/KnockOff/src/TestClock/TestClock.cs", + "description": "File TestClock.cs ", + "pattern": "this is the code" + } + + ,{ + "file": "rscg_examples/KnockOff/src/Mock/IMyClock.cs", + "description": "File IMyClock.cs ", + "pattern": "this is the code" + } + + + ,{ + "file": "rscg_examples/KnockOff/src/TestClock/obj/GX/KnockOff.Generator/KnockOff.KnockOffGenerator/QuickStartRepoStub.g.cs", + "description": "Generated File 2 from 2 : QuickStartRepoStub.g.cs ", + "line": 1 + } + + ,{ + "file": "rscg_examples/KnockOff/src/TestClock/obj/GX/KnockOff.Generator/KnockOff.KnockOffGenerator/QuickStartRepoStub.Base.g.cs", + "description": "Generated File 1 from 2 : QuickStartRepoStub.Base.g.cs ", + "line": 1 + } + + ], + + "ref": "main" + +} \ No newline at end of file diff --git a/v2/Generator/DocusaurusExample.txt b/v2/Generator/DocusaurusExample.txt index 270eff4d3..1a829302e 100644 --- a/v2/Generator/DocusaurusExample.txt +++ b/v2/Generator/DocusaurusExample.txt @@ -49,7 +49,7 @@ Source: {{ Description.Generator.Source }} {{image}} ::: -### Original Readme +## Original Readme :::note {{Description.OriginalReadme}} diff --git a/v2/Generator/MultiGeneratorV2.cs b/v2/Generator/MultiGeneratorV2.cs index 3155385be..1882bc3c0 100644 --- a/v2/Generator/MultiGeneratorV2.cs +++ b/v2/Generator/MultiGeneratorV2.cs @@ -224,9 +224,14 @@ public string[] SourceNoRSCG() text = text.Replace("(LICENSE.TXT)", $"({d.Generator!.Source}/LICENSE.TXT)"); text = text.Replace("(docs/building.md)", $"({d.Generator!.Source}/docs/building.md)"); text = text.Replace("(./", $"({d.Generator!.Source}/"); - + text = text.Replace("(skills/", $"({d.Generator!.Source}/skills"); text = text.Replace("Access them as a ReadOnlySpan", "Access them as a ReadOnlySpan\\"); + text = text.Replace("### ", "##### "); + text = text.Replace("## ", "#### "); + text = text.Replace("# ", "### "); + text= text.Replace("C###", "C#"); + return text; } ; diff --git a/v2/Generator/all.csv b/v2/Generator/all.csv index 003f38a36..7623fc5a3 100644 --- a/v2/Generator/all.csv +++ b/v2/Generator/all.csv @@ -253,3 +253,4 @@ Nr,Key,Source,Category 252,RSCG_idempotency, https://github.com/ignatandrei/RSCG_idempotency,Idempotency 253,FastCloner, https://github.com/lofcz/FastCloner/,Clone 254,ErrorOrX, https://github.com/ANcpLua/ErrorOrX,API +255,KnockOff, https://github.com/NeatooDotNet/KnockOff,Tests diff --git a/v2/RSCGExamplesData/GeneratorDataRec.json b/v2/RSCGExamplesData/GeneratorDataRec.json index 62b7082ba..baa03019d 100644 --- a/v2/RSCGExamplesData/GeneratorDataRec.json +++ b/v2/RSCGExamplesData/GeneratorDataRec.json @@ -1540,5 +1540,11 @@ "Category": 15, "dtStart": "2026-02-02T00:00:00", "show": true + }, + { + "ID": "KnockOff", + "Category": 13, + "dtStart": "2026-02-13T00:00:00", + "show": true } ] \ No newline at end of file diff --git a/v2/book/examples/ErrorOrX.html b/v2/book/examples/ErrorOrX.html index bf8047088..f0be402a8 100644 --- a/v2/book/examples/ErrorOrX.html +++ b/v2/book/examples/ErrorOrX.html @@ -4,11 +4,11 @@

RSCG nr 254 : ErrorOrX

Info

Nuget : https://www.nuget.org/packages/ErrorOrX/ -

You can find more details at : https://github.com/ANcpLua/ErrorOrX/

+

You can find more details at : https://github.com/ANcpLua/ErrorOrX

Author :Alexander Nachtmanns

-

Source: https://github.com/ANcpLua/ErrorOrX/

+

Source: https://github.com/ANcpLua/ErrorOrX

About

diff --git a/v2/book/examples/KnockOff.html b/v2/book/examples/KnockOff.html new file mode 100644 index 000000000..a61844820 --- /dev/null +++ b/v2/book/examples/KnockOff.html @@ -0,0 +1,64 @@ + +

RSCG nr 255 : KnockOff

+ +

Info

+Nuget : https://www.nuget.org/packages/KnockOff/ + +

You can find more details at : https://github.com/NeatooDotNet/KnockOff

+ +

Author :Keith Voels

+ +

Source: https://github.com/NeatooDotNet/KnockOff

+ +

About

+ +Generating test stubs with mocking for interfaces + +

+ How to use +

+

+ Add reference to the KnockOff in the csproj +

+ + +

This was for me the starting code

+ +
+ I have coded the file IMyClock.cs +
+ +
+ +
+ I have coded the file TestClock.cs +
+ +
+

And here are the generated files

+ +
+ The file generated is QuickStartRepoStub.Base.g.cs +
+ + +
+ The file generated is QuickStartRepoStub.g.cs +
+ + +

+ You can download the code and this page as pdf from + + https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff + +

+ + +

+ You can see the whole list at + + https://ignatandrei.github.io/RSCG_Examples/v2/docs/List-of-RSCG + +

+ diff --git a/v2/book/list.html b/v2/book/list.html index 957e64006..71c549521 100644 --- a/v2/book/list.html +++ b/v2/book/list.html @@ -17,7 +17,7 @@

-This is the list of 254 RSCG with examples => +This is the list of 255 RSCG with examples =>

@@ -1042,6 +1042,10 @@

+ + + +
254 ErrorOrX
255KnockOff
diff --git a/v2/book/pandocHTML.yaml b/v2/book/pandocHTML.yaml index 0e2720b7a..be7df0edb 100644 --- a/v2/book/pandocHTML.yaml +++ b/v2/book/pandocHTML.yaml @@ -268,6 +268,7 @@ input-files: - examples/RSCG_idempotency.html - examples/FastCloner.html - examples/ErrorOrX.html +- examples/KnockOff.html # or you may use input-file: with a single value # defaults: diff --git a/v2/rscg_examples/KnockOff/description.json b/v2/rscg_examples/KnockOff/description.json new file mode 100644 index 000000000..fa4db2ea3 --- /dev/null +++ b/v2/rscg_examples/KnockOff/description.json @@ -0,0 +1,22 @@ +{ + "generator":{ + "name":"KnockOff", + "nuget":[ + "https://www.nuget.org/packages/KnockOff/" + ], + "link":"https://github.com/NeatooDotNet/KnockOff", + "author":"Keith Voels", + "source":"https://github.com/NeatooDotNet/KnockOff" + }, + "data":{ + "goodFor":["Generating test stubs with mocking for interfaces"], + "csprojDemo":"TestClock.csproj", + "csFiles":["IMyClock.cs","TestClock.cs"], + "excludeDirectoryGenerated":[""], + "includeAdditionalFiles":[""] + }, + "links":{ + "blog":"", + "video":"" + } +} \ No newline at end of file diff --git a/v2/rscg_examples/KnockOff/nuget.txt b/v2/rscg_examples/KnockOff/nuget.txt new file mode 100644 index 000000000..12db84572 --- /dev/null +++ b/v2/rscg_examples/KnockOff/nuget.txt @@ -0,0 +1 @@ +A Roslyn Source Generator for creating unit test stubs. Unlike Moq's fluent runtime configuration, KnockOff uses partial classes for compile-time setup—trading flexibility for readability and performance. \ No newline at end of file diff --git a/v2/rscg_examples/KnockOff/readme.txt b/v2/rscg_examples/KnockOff/readme.txt new file mode 100644 index 000000000..acc677248 --- /dev/null +++ b/v2/rscg_examples/KnockOff/readme.txt @@ -0,0 +1,529 @@ +# KnockOff + +A .NET mocking library that lets you define reusable stub classes — with full mocking capabilities built in. + +Define your test double once. Reuse it across your test project. Customize it per-test with Return, Call, Verify, and When chains. No more copying mock setups between tests or maintaining shared factory methods full of `Arg.Any<>()`. + +Powered by Roslyn source generation for [tighter type safety](docs/type-safety.md) — more issues surface as compile errors instead of runtime surprises. + +Claude Code was used to write this library. Skip to more [AI discussion](#ai). + +[![NuGet](https://img.shields.io/nuget/v/KnockOff.svg)](https://www.nuget.org/packages/KnockOff/) +[![Build Status](https://github.com/NeatooDotNet/KnockOff/workflows/Build,%20Test%20&%20Publish/badge.svg)](https://github.com/NeatooDotNet/KnockOff/actions) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +## KnockOff Stub + +There are [9 patterns](docs/guides/stub-patterns.md) total, including a [standard fluent mocking approach](docs/guides/stub-patterns.md#inline-interface-pattern) with inline stubs. But reusable stub classes are where KnockOff stands apart: + + +```cs +[KnockOff] +public partial class MyRepoStub(List Users) : IMyRepo +{ + protected override User? GetUser_(int id) + { + return Users.Single(u => u.Id == id); + } + + protected override void Update_(User user) + { + Assert.Contains(user, Users); + } +} +``` + + +- **`[KnockOff]` + `partial class`** — KnockOff generates a base class that implements every member of `IMyRepo`. Your stub is a real class — define it once, reuse it across your entire test project. Pass it around, register it in DI, share it between test fixtures. +- **Constructor parameters** — `List Users` is a primary constructor. Test data flows in naturally, just like any other C# class. +- **Overrides are optional** — `GetUser_` and `Update_` override the generated defaults. Only override what you need — everything else still works with [Return/Call](docs/guides/methods.md), [Return(value)](docs/reference/interceptor-api.md), or [When chains](docs/guides/parameter-matching.md). +- **Tighter type safety** — Every Return, Call, and When call is complete in a single step — no forgotten `.Returns()` that [silently breaks at runtime](docs/type-safety.md). No manual `` type parameters that can drift. [Details →](docs/type-safety.md) + +This stub is also a full mock. It has [Verify](docs/guides/verification.md), [Strict mode](docs/guides/strict-mode.md), [Async](docs/guides/async-patterns.md), and [Source Delegation](docs/guides/source-delegation.md) — all on the same reusable class. + + +## Why I Wrote KnockOff + +I often wanted to reuse my mocks. +Especially in my integration test library where I may even register my mocks. +I found myself either copying my mock definitions code or creating shared methods like this: + +**NSubstitute:** + +```cs +public static IMyRepo NSubstituteMock(List users) +{ + var myRepoMock = Substitute.For(); + + // Setup: configure GetUser to look up from the list based on id + myRepoMock.GetUser(Arg.Any()) + .Returns(callInfo => users.SingleOrDefault(u => u.Id == callInfo.Arg())); + + // Setup: configure Update to assert user exists in list + myRepoMock.When(x => x.Update(Arg.Any())) + .Do(callInfo => Assert.Contains(callInfo.Arg(), users)); + + return myRepoMock; +} +``` + + +Here's another [example from PowerToys](https://github.com/microsoft/PowerToys/blob/main/src/settings-ui/Settings.UI.UnitTests/Mocks/ISettingsUtilsMocks.cs). + +But I find that hard to read and unintuitive. Also, my shared methods accumulated extra parameters for variations across different tests. + + +## So I Created KnockOff + +You can create a stub to implement [interfaces](docs/guides/stub-patterns.md) or non-sealed [classes](docs/guides/stub-patterns.md) with virtual methods. +Yet, you can still customize the stub per test. +All while having the features you would expect with a full mocking library. + +With the stub above, your tests are: + + +```cs +var myRepoKO = new MyRepoStub([new User { Id = 1 }, new User { Id = 2 }]); +var userDomainModel = new UserDomainModel(myRepoKO); + +Assert.True(userDomainModel.Fetch(1)); + +// I have Verify on my Stub! +myRepoKO.GetUser.Verify(Called.Once); +``` + + +Need different behavior for a specific test? Override with Return/Call: + + +```cs +var user1 = new User { Id = 1 }; // Ignored do to per-test configuration +var myRepoKO = new MyRepoStub([user1]); +var userDomainModel = new UserDomainModel(myRepoKO); + +var user2 = new User { Id = 2 }; + +// When and Return overrides the stub methods +myRepoKO.GetUser.When(2).Return(user2).Verifiable(); +myRepoKO.Update.Call(u => Assert.Same(u, user2)).Verifiable(); + +userDomainModel.Fetch(2); +userDomainModel.Update(); + +myRepoKO.Verify(); +``` + + +**Now I have my stubs and mocks in one!** + +--- + +## What Sets KnockOff Apart + +- **[Reusable stub classes](docs/guides/reusable-stubs.md)** — Define once, customize per-test. Your stub is a real class — pass it through constructors, register it in DI. +- **[Source delegation](docs/guides/source-delegation.md)** — Delegate to a real implementation, override only specific methods. No equivalent in Moq or NSubstitute. +- **[Protected methods](docs/guides/protected-methods.md)** — Same `Return`/`Call`/`Verify` API, fully typed. No string-based names, no manual subclasses. +- **[Ref/out parameters](docs/guides/ref-out-parameters.md)** — Natural lambda syntax with `ref`/`out` keywords. No special matchers or index-based access. +- **[Multiple interfaces](docs/guides/multiple-interfaces.md)** — Unified interceptors on one stub. No `.As()` references or casting. +- **[Tighter type safety](docs/type-safety.md)** — Each Return/Call/When call is complete in one step — no forgotten `.Returns()` that silently breaks at runtime. +- **[Parameter matching](docs/guides/parameter-matching-comparison.md)** — `Return((a, b) => a > 0 ? 100 : 0)` — standard C# conditionals instead of `Arg.Is<>` or `It.Is<>` per parameter. +- **Built-in argument capture** — `LastArg`, `LastArgs`, `LastSetValue`, `LastSetEntry` — no manual `Arg.Do<>` or `Callback<>` setup. +- **Event verification** — `VerifyAdd()` / `VerifyRemove()` / `HasSubscribers` — not available in Moq or NSubstitute. +- **Explicit Get/Set verification** — `VerifyGet(Called)` / `VerifySet(Called)` for properties and indexers. +- **Stubbing concrete classes** — Override virtual methods on non-sealed classes with the same API. + +--- + +## Quick Start + +### Install + +```bash +dotnet add package KnockOff +``` + +### Create a Stub + + +```cs +public interface IQuickStartRepo +{ + User? GetUser(int id); +} + +[KnockOff] +public partial class QuickStartRepoStub : IQuickStartRepo { } + +public class QuickStartCreateStubTests +{ + [Fact] + public void CreateStub_IsReady() + { + var stub = new QuickStartRepoStub(); + + IQuickStartRepo repository = stub; + Assert.NotNull(repository); + } +} +``` + + +### Configure and Verify + + +```cs +[Fact] +public void ConfigureStub_WithReturn() +{ + var stub = new QuickStartRepoStub(); + + stub.GetUser.Return((id) => new User { Id = id, Name = "Test User" }); + + IQuickStartRepo repository = stub; + var user = repository.GetUser(42); + + Assert.NotNull(user); + Assert.Equal(42, user.Id); + Assert.Equal("Test User", user.Name); +} +``` + + + +```cs +[Fact] +public void VerifyCalls_WithVerifiable() +{ + var stub = new QuickStartRepoStub(); + stub.GetUser.Return((id) => new User { Id = id, Name = "Test" }).Verifiable(); + + IQuickStartRepo repository = stub; + + var user = repository.GetUser(42); + + // Verify() checks all members marked with .Verifiable() + stub.Verify(); +} +``` + + +--- + +## The Difference + +**Moq:** +```cs +mock.Setup(x => x.GetUser(It.Is(id => id > 0))) + .Returns(id => new User { Id = id }); +``` + +**NSubstitute:** + +```cs +var repo = Substitute.For(); +repo.GetUser(Arg.Is(id => id > 0)).Returns(x => new User { Id = x.Arg() }); +``` + + +**KnockOff:** + +```cs +var stub = new CompareUserRepoStub(); +stub.GetUser.Return((id) => id > 0 ? new User { Id = id } : null); +``` + + +No `It.Is<>()`. No `Arg.Is<>()`. No `x.Arg()`. The parameter is just `id`. + +--- + +For side-by-side comparison tables (methods, properties, events, delegates, indexers), see the [complete comparison guide](docs/comparison.md). + +--- + +## Argument Matching + +**Moq:** +```cs +// Moq - It.Is per parameter +mock.Setup(x => x.Add(It.Is(a => a > 0), It.IsAny())).Returns(100); +``` + +**NSubstitute:** + +```cs +// NSubstitute - Arg.Is per parameter (permanent matchers) +calc.Add(Arg.Is(a => a > 0), Arg.Any()).Returns(100); +``` + + +**KnockOff:** + +```cs +// KnockOff - Returns with conditional (permanent, matches all calls) +stub.Add.Return((a, b) => a > 0 ? 100 : 0); +``` + + + +```cs +// KnockOff - When() for sequential matching (first match returns 100, then falls through) +stub.Add.When((a, b) => a > 0).Return(100).ThenCall((a, b) => a + b); +``` + + +**Multiple specific values:** + +**Moq:** +```cs +mock.Setup(x => x.Add(1, 2)).Returns(100); +mock.Setup(x => x.Add(3, 4)).Returns(200); +``` + + +```cs +// Multiple specific values +calc.Add(1, 2).Returns(100); +calc.Add(3, 4).Returns(200); +``` + + + +```cs +stub.Add.When(1, 2).Return(100); +stub.Add.When(3, 4).Return(200); +``` + + +**Note:** Moq and NSubstitute matchers are permanent -- they match all qualifying calls. KnockOff's `When()` is sequential -- matchers are consumed in order. Use `Return(callback)` with conditionals for permanent matching behavior. + +### Argument Capture + +**Moq:** +```cs +// Moq - requires Callback setup +int capturedA = 0, capturedB = 0; +mock.Setup(x => x.Add(It.IsAny(), It.IsAny())) + .Callback((a, b) => { capturedA = a; capturedB = b; }); +mock.Object.Add(1, 2); +``` + +**NSubstitute:** + +```cs +// NSubstitute - requires Arg.Do in setup +int capturedA = 0, capturedB = 0; +calc.Add(Arg.Do(x => capturedA = x), Arg.Do(x => capturedB = x)); +calc.Add(1, 2); +``` + + +**KnockOff:** + +```cs +// KnockOff - built-in, no pre-setup +var tracking = stub.Add.Return((a, b) => a + b); +ICalculator calc = stub; +calc.Add(1, 2); +var (a, b) = tracking.LastArgs; // Named tuple: a = 1, b = 2 +``` + + +For full comparisons of properties, events, delegates, and indexers, see the [complete comparison guide](docs/comparison.md). + +--- + +## Method Overload Resolution + +**The Problem:** When an interface has overloaded methods with the same parameter count but different types: + + +```cs +public interface IFormatter +{ + string Format(string input, bool uppercase); + string Format(string input, int maxLength); +} +``` + + +### Any-Value Matching + +**Moq:** +```cs +// It.IsAny() required - compiler needs the types to resolve overload +mock.Setup(x => x.Format(It.IsAny(), It.IsAny())).Returns("bool overload"); +mock.Setup(x => x.Format(It.IsAny(), It.IsAny())).Returns("int overload"); +``` + +**NSubstitute:** + +```cs +// Arg.Any() required - compiler needs the types to resolve overload +formatter.Format(Arg.Any(), Arg.Any()).Returns("bool overload"); +formatter.Format(Arg.Any(), Arg.Any()).Returns("int overload"); +``` + + +**KnockOff:** + +```cs +// Explicit parameter types resolve the overload - standard C# syntax +stub.Format.Return((string input, bool uppercase) => "bool overload"); +stub.Format.Return((string input, int maxLength) => "int overload"); +``` + + +### Specific-Value Matching + +**NSubstitute:** + +```cs +// Specific value matching - literals work when all args are specific +formatter.Format("test", true).Returns("UPPERCASE"); +formatter.Format("test", 10).Returns("truncated"); +``` + + +**KnockOff:** + +```cs +// Specific value matching - parameter types resolve the overload +stub.Format.When("test", true).Return("UPPERCASE"); +stub.Format.When("test", 10).Return("truncated"); +``` + + +### Argument Access + +**Moq:** +```cs +// To use argument values, extract via Returns: +mock.Setup(x => x.Format(It.IsAny(), It.IsAny())) + .Returns((input, uppercase) => uppercase ? input.ToUpper() : input); +``` + +**NSubstitute:** + +```cs +// To use argument values, extract from CallInfo: +formatter.Format(Arg.Any(), Arg.Any()) + .Returns(x => x.ArgAt(1) ? x.ArgAt(0).ToUpper() : x.ArgAt(0)); +``` + + +**KnockOff:** + +```cs +// Arguments are directly available with names and types: +stub.Format.Return((string input, bool uppercase) => uppercase ? input.ToUpper() : input); +``` + + +**The Difference:** +- Moq: `It.IsAny()` + `.Returns((input, uppercase) => ...)` to match any value and access arguments +- NSubstitute: `Arg.Any()` + `x.ArgAt(1)` to match any value and access arguments +- KnockOff: `(string input, bool uppercase)` - standard C# lambda with named, typed parameters + +--- + +## Three Stub Patterns + +KnockOff supports [9 patterns](docs/guides/stub-patterns.md) total. Here are the three most common: + +**[Standalone](docs/guides/stub-patterns.md#standalone-pattern)** - Reusable across your project: + +```cs +[KnockOff] +public partial class ReadmeStandaloneStub : IUserRepo { } +``` + + +**[Inline Interface](docs/guides/stub-patterns.md#inline-interface-pattern)** - Test-local stubs: + +```cs +[Fact] +public void InlineInterface_Pattern() +{ + var stub = new Stubs.IUserRepo(); + stub.GetUser.Return((id) => new User { Id = id }); + + IUserRepo repo = stub; + Assert.NotNull(repo.GetUser(1)); +} +``` + + +**[Inline Class](docs/guides/stub-patterns.md#inline-class-pattern)** - Stub virtual members: + +```cs +[Fact] +public void InlineClass_Pattern() +{ + var stub = new Stubs.MyService(); + stub.GetUser.Return((id) => new User { Id = id }); + + MyService service = stub.Object; + Assert.NotNull(service.GetUser(1)); +} +``` + + +--- + +## Roslyn Source Generation + +KnockOff uses Roslyn source generation, which means: + +- No more `Arg.Any<>()`. No more `It.IsAny<>()`. Just write C# +- If the method signature changes you get a compile error +- There's a small performance gain but honestly it's negligible + +Source generation opens doors beyond traditional mocking — I've already added [9 patterns](docs/guides/stub-patterns.md) and features like [Source Delegation](docs/guides/source-delegation.md), with more ideas to come. + +**What other ideas do you have?** Open a [discussion](https://github.com/NeatooDotNet/KnockOff/discussions). + + +## AI + +This is an idea I've had for years but never took the time to implement. With my ideas and guidance, Claude Code has written the entirety of this library — the Roslyn source generator, the runtime library, the tests, and the documentation. + +Source generation turned out to be a great fit for AI code generation. The work is highly patterned: analyze an interface, generate code for each member, handle edge cases across 9 patterns and 4 member types. That's exactly the kind of systematic, repetitive-but-varied work where AI excels. I designed the API and patterns; Claude Code implemented them across every combination. + +### Claude Code Skill + +KnockOff includes a [Claude Code skill](skills/knockoff/) that teaches Claude how to use the library. Copy the `skills/knockoff/` directory into your project and Claude Code will know how to create stubs, configure behavior, write tests with KnockOff, and migrate from Moq — without you explaining the API. + +The skill includes slash commands: +- **`/knockoff:create-stub`** — Create a new stub class with the pattern of your choice +- **`/knockoff:migrate-from-moq`** — Convert existing Moq tests to KnockOff +- **`/knockoff:troubleshoot`** — Diagnose and fix common KnockOff issues + +--- + +## Documentation + +- **[Getting Started](docs/getting-started.md)** - Installation and first stub +- **[Stub Patterns](docs/guides/stub-patterns.md)** - Standalone, inline interface, inline class +- **[Interceptor API](docs/reference/interceptor-api.md)** - Complete `Returns`, `Execute`, `Get`, `Set` reference +- **[Source Delegation](docs/guides/source-delegation.md)** - Delegate to real implementations +- **[Full Comparison Guide](docs/comparison.md)** - Properties, events, delegates, indexers vs Moq and NSubstitute +- **[Migration from Moq](docs/migration/from-moq.md)** - Step-by-step migration guide +- **[Migration from NSubstitute](docs/migration/from-nsubstitute.md)** - Comparison and migration guide + +--- + +## License + +MIT License. See [LICENSE](LICENSE) for details. + +--- + +## Contributing + +Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +- **Issues**: [GitHub Issues](https://github.com/NeatooDotNet/KnockOff/issues) +- **Pull Requests**: Bug fixes, features, documentation +- **Discussions**: [GitHub Discussions](https://github.com/NeatooDotNet/KnockOff/discussions) diff --git a/v2/rscg_examples/KnockOff/src/Mock/IMyClock.cs b/v2/rscg_examples/KnockOff/src/Mock/IMyClock.cs new file mode 100644 index 000000000..b5f9f222d --- /dev/null +++ b/v2/rscg_examples/KnockOff/src/Mock/IMyClock.cs @@ -0,0 +1,7 @@ +namespace MockData; + +public interface IMyClock +{ + public DateTime GetNow(); + public DateTime GetUtcNow(); +} \ No newline at end of file diff --git a/v2/rscg_examples/KnockOff/src/Mock/MockData.csproj b/v2/rscg_examples/KnockOff/src/Mock/MockData.csproj new file mode 100644 index 000000000..9ed914b5b --- /dev/null +++ b/v2/rscg_examples/KnockOff/src/Mock/MockData.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/v2/rscg_examples/KnockOff/src/MockRock.slnx b/v2/rscg_examples/KnockOff/src/MockRock.slnx new file mode 100644 index 000000000..e2f5c864b --- /dev/null +++ b/v2/rscg_examples/KnockOff/src/MockRock.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/v2/rscg_examples/KnockOff/src/TestClock/TestClock.cs b/v2/rscg_examples/KnockOff/src/TestClock/TestClock.cs new file mode 100644 index 000000000..ae733f450 --- /dev/null +++ b/v2/rscg_examples/KnockOff/src/TestClock/TestClock.cs @@ -0,0 +1,26 @@ + +using KnockOff; + +namespace TestClock; + +[KnockOff] +public partial class QuickStartRepoStub : IMyClock { } + + +[TestClass] +public class TestClock +{ + [TestMethod] + public void TestMyClock() + { + var expectations = new QuickStartRepoStub(); + expectations.GetNow.Return(DateTime.Now.AddYears(-1)); + + IMyClock mock = expectations; + var data= mock.GetNow(); + Assert.AreEqual(DateTime.Now.Year -1, data.Year); + expectations.Verify(); + } +} + + diff --git a/v2/rscg_examples/KnockOff/src/TestClock/TestClock.csproj b/v2/rscg_examples/KnockOff/src/TestClock/TestClock.csproj new file mode 100644 index 000000000..69ea89824 --- /dev/null +++ b/v2/rscg_examples/KnockOff/src/TestClock/TestClock.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + true + $(BaseIntermediateOutputPath)\GX + + + diff --git a/v2/rscg_examples/KnockOff/src/TestClock/Usings.cs b/v2/rscg_examples/KnockOff/src/TestClock/Usings.cs new file mode 100644 index 000000000..f3be1f99c --- /dev/null +++ b/v2/rscg_examples/KnockOff/src/TestClock/Usings.cs @@ -0,0 +1,2 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using MockData; diff --git a/v2/rscg_examples/KnockOff/video.json b/v2/rscg_examples/KnockOff/video.json new file mode 100644 index 000000000..ebcf74453 --- /dev/null +++ b/v2/rscg_examples/KnockOff/video.json @@ -0,0 +1,39 @@ +{ + "scriptName": "KnockOff", + "steps": +[ + {"typeStep":"exec","arg":"clipchamp.exe launch"}, + {"typeStep":"text","arg": "Welcome to Roslyn Examples"}, + {"typeStep":"text","arg":"If you want to see more examples , see List Of RSCG"}, + {"typeStep":"browser","arg":"https://ignatandrei.github.io/RSCG_Examples/v2/docs/List-of-RSCG"}, + {"typeStep":"text","arg": "My name is Andrei Ignat and I am deeply fond of Roslyn Source Code Generator. "}, + +{"typeStep":"text","arg": "Today I will present KnockOff . Generating test stubs with mocking for interfaces ."}, +{"typeStep":"browser","arg":"https://www.nuget.org/packages/KnockOff/"}, +{"typeStep":"text","arg": "The whole example is here"}, +{"typeStep":"browser","arg":"https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff"}, +{"typeStep":"text","arg": "You can download the code from here"}, +{"typeStep":"browser","arg":"https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff#download-example-net--c-"}, +{"typeStep":"text","arg":"Here is the code downloaded "}, +{"typeStep":"exec","arg":"explorer.exe /select,D:\\gth\\RSCG_Examples\\v2\\Generator.sln"}, +{"typeStep":"text","arg": "So , let's start the project with Visual Studio Code "}, +{"typeStep":"stepvscode","arg": "-n D:\\gth\\RSCG_Examples\\v2"}, + +{"typeStep":"text","arg": "To use it ,you will put the Nuget KnockOff into the csproj "}, + +{"typeStep":"stepvscode","arg": "-r -g D:\\gth\\RSCG_Examples\\v2\\rscg_examples\\KnockOff\\src\\TestClock\\TestClock.csproj"}, + +{"typeStep":"text","arg": "And now I will show you an example of using KnockOff"}, + +{"typeStep":"hide","arg": "now execute the tour in VSCode"}, +{"typeStep":"tour", "arg": "src/.tours/"}, +{"typeStep":"text","arg":" And I will execute the project"}, +{"typeStep":"showproj", "arg":"TestClock.csproj"}, +{"typeStep":"text","arg":" This concludes the project"}, +{"typeStep":"waitseconds","arg":"30"}, +{"typeStep":"text","arg": "Remember, you can download the code from here"}, +{"typeStep":"browser","arg":"https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff#download-example-net--c-", +SpeakTest=" "}, +{"typeStep":"waitseconds","arg":"30"}, +] +} diff --git a/v2/rscg_examples_site/docs/Authors/Keith_Voels.md b/v2/rscg_examples_site/docs/Authors/Keith_Voels.md new file mode 100644 index 000000000..7f2b1ac1c --- /dev/null +++ b/v2/rscg_examples_site/docs/Authors/Keith_Voels.md @@ -0,0 +1,7 @@ +# Author : Keith Voels + +Number RSCG: 1 + + + 1 [KnockOff](/docs/KnockOff) [![Nuget](https://img.shields.io/nuget/dt/KnockOff?label=KnockOff)](https://www.nuget.org/packages/KnockOff/) ![GitHub Repo stars](https://img.shields.io/github/stars/NeatooDotNet/KnockOff?style=social) 2026-02-13 + diff --git a/v2/rscg_examples_site/docs/Categories/Tests.md b/v2/rscg_examples_site/docs/Categories/Tests.md index 11c4931b8..211778688 100644 --- a/v2/rscg_examples_site/docs/Categories/Tests.md +++ b/v2/rscg_examples_site/docs/Categories/Tests.md @@ -1,18 +1,20 @@

Tests

-Number RSCG: 7 +Number RSCG: 8 1 [Imposter](/docs/Imposter) [![Nuget](https://img.shields.io/nuget/dt/Imposter?label=Imposter)](https://www.nuget.org/packages/Imposter/) ![GitHub Repo stars](https://img.shields.io/github/stars/themidnightgospel/Imposter?style=social) 2025-12-13 - 2 [mocklis](/docs/mocklis) [![Nuget](https://img.shields.io/nuget/dt/mocklis?label=mocklis)](https://www.nuget.org/packages/mocklis/) ![GitHub Repo stars](https://img.shields.io/github/stars/mocklis/mocklis?style=social) 2024-01-03 + 2 [KnockOff](/docs/KnockOff) [![Nuget](https://img.shields.io/nuget/dt/KnockOff?label=KnockOff)](https://www.nuget.org/packages/KnockOff/) ![GitHub Repo stars](https://img.shields.io/github/stars/NeatooDotNet/KnockOff?style=social) 2026-02-13 - 3 [MockMe](/docs/MockMe) [![Nuget](https://img.shields.io/nuget/dt/MockMe?label=MockMe)](https://www.nuget.org/packages/MockMe/) ![GitHub Repo stars](https://img.shields.io/github/stars/connorivy/MockMe?style=social) 2025-02-10 + 3 [mocklis](/docs/mocklis) [![Nuget](https://img.shields.io/nuget/dt/mocklis?label=mocklis)](https://www.nuget.org/packages/mocklis/) ![GitHub Repo stars](https://img.shields.io/github/stars/mocklis/mocklis?style=social) 2024-01-03 - 4 [MSTest](/docs/MSTest) [![Nuget](https://img.shields.io/nuget/dt/MSTest.SourceGeneration?label=MSTest.SourceGeneration)](https://www.nuget.org/packages/MSTest.SourceGeneration/) ![GitHub Repo stars](https://img.shields.io/github/stars/microsoft/testfx?style=social) 2024-04-04 + 4 [MockMe](/docs/MockMe) [![Nuget](https://img.shields.io/nuget/dt/MockMe?label=MockMe)](https://www.nuget.org/packages/MockMe/) ![GitHub Repo stars](https://img.shields.io/github/stars/connorivy/MockMe?style=social) 2025-02-10 - 5 [Ridge](/docs/Ridge) [![Nuget](https://img.shields.io/nuget/dt/Ridge?label=Ridge)](https://www.nuget.org/packages/Ridge/) ![GitHub Repo stars](https://img.shields.io/github/stars/Melchy/Ridge?style=social) 2023-08-20 + 5 [MSTest](/docs/MSTest) [![Nuget](https://img.shields.io/nuget/dt/MSTest.SourceGeneration?label=MSTest.SourceGeneration)](https://www.nuget.org/packages/MSTest.SourceGeneration/) ![GitHub Repo stars](https://img.shields.io/github/stars/microsoft/testfx?style=social) 2024-04-04 - 6 [Rocks](/docs/Rocks) [![Nuget](https://img.shields.io/nuget/dt/Rocks?label=Rocks)](https://www.nuget.org/packages/Rocks/) ![GitHub Repo stars](https://img.shields.io/github/stars/JasonBock/Rocks?style=social) 2023-04-16 + 6 [Ridge](/docs/Ridge) [![Nuget](https://img.shields.io/nuget/dt/Ridge?label=Ridge)](https://www.nuget.org/packages/Ridge/) ![GitHub Repo stars](https://img.shields.io/github/stars/Melchy/Ridge?style=social) 2023-08-20 - 7 [TUnit](/docs/TUnit) [![Nuget](https://img.shields.io/nuget/dt/TUnit?label=TUnit)](https://www.nuget.org/packages/TUnit/) ![GitHub Repo stars](https://img.shields.io/github/stars/thomhurst/TUnit?style=social) 2025-11-08 + 7 [Rocks](/docs/Rocks) [![Nuget](https://img.shields.io/nuget/dt/Rocks?label=Rocks)](https://www.nuget.org/packages/Rocks/) ![GitHub Repo stars](https://img.shields.io/github/stars/JasonBock/Rocks?style=social) 2023-04-16 + + 8 [TUnit](/docs/TUnit) [![Nuget](https://img.shields.io/nuget/dt/TUnit?label=TUnit)](https://www.nuget.org/packages/TUnit/) ![GitHub Repo stars](https://img.shields.io/github/stars/thomhurst/TUnit?style=social) 2025-11-08 \ No newline at end of file diff --git a/v2/rscg_examples_site/docs/Categories/_PrimitiveTests.mdx b/v2/rscg_examples_site/docs/Categories/_PrimitiveTests.mdx index 816acc669..7b4c6c54e 100644 --- a/v2/rscg_examples_site/docs/Categories/_PrimitiveTests.mdx +++ b/v2/rscg_examples_site/docs/Categories/_PrimitiveTests.mdx @@ -2,17 +2,19 @@ 1 [Imposter](/docs/Imposter) [![Nuget](https://img.shields.io/nuget/dt/Imposter?label=Imposter)](https://www.nuget.org/packages/Imposter/) ![GitHub Repo stars](https://img.shields.io/github/stars/themidnightgospel/Imposter?style=social) 2025-12-13 - 2 [mocklis](/docs/mocklis) [![Nuget](https://img.shields.io/nuget/dt/mocklis?label=mocklis)](https://www.nuget.org/packages/mocklis/) ![GitHub Repo stars](https://img.shields.io/github/stars/mocklis/mocklis?style=social) 2024-01-03 + 2 [KnockOff](/docs/KnockOff) [![Nuget](https://img.shields.io/nuget/dt/KnockOff?label=KnockOff)](https://www.nuget.org/packages/KnockOff/) ![GitHub Repo stars](https://img.shields.io/github/stars/NeatooDotNet/KnockOff?style=social) 2026-02-13 - 3 [MockMe](/docs/MockMe) [![Nuget](https://img.shields.io/nuget/dt/MockMe?label=MockMe)](https://www.nuget.org/packages/MockMe/) ![GitHub Repo stars](https://img.shields.io/github/stars/connorivy/MockMe?style=social) 2025-02-10 + 3 [mocklis](/docs/mocklis) [![Nuget](https://img.shields.io/nuget/dt/mocklis?label=mocklis)](https://www.nuget.org/packages/mocklis/) ![GitHub Repo stars](https://img.shields.io/github/stars/mocklis/mocklis?style=social) 2024-01-03 - 4 [MSTest](/docs/MSTest) [![Nuget](https://img.shields.io/nuget/dt/MSTest.SourceGeneration?label=MSTest.SourceGeneration)](https://www.nuget.org/packages/MSTest.SourceGeneration/) ![GitHub Repo stars](https://img.shields.io/github/stars/microsoft/testfx?style=social) 2024-04-04 + 4 [MockMe](/docs/MockMe) [![Nuget](https://img.shields.io/nuget/dt/MockMe?label=MockMe)](https://www.nuget.org/packages/MockMe/) ![GitHub Repo stars](https://img.shields.io/github/stars/connorivy/MockMe?style=social) 2025-02-10 - 5 [Ridge](/docs/Ridge) [![Nuget](https://img.shields.io/nuget/dt/Ridge?label=Ridge)](https://www.nuget.org/packages/Ridge/) ![GitHub Repo stars](https://img.shields.io/github/stars/Melchy/Ridge?style=social) 2023-08-20 + 5 [MSTest](/docs/MSTest) [![Nuget](https://img.shields.io/nuget/dt/MSTest.SourceGeneration?label=MSTest.SourceGeneration)](https://www.nuget.org/packages/MSTest.SourceGeneration/) ![GitHub Repo stars](https://img.shields.io/github/stars/microsoft/testfx?style=social) 2024-04-04 - 6 [Rocks](/docs/Rocks) [![Nuget](https://img.shields.io/nuget/dt/Rocks?label=Rocks)](https://www.nuget.org/packages/Rocks/) ![GitHub Repo stars](https://img.shields.io/github/stars/JasonBock/Rocks?style=social) 2023-04-16 + 6 [Ridge](/docs/Ridge) [![Nuget](https://img.shields.io/nuget/dt/Ridge?label=Ridge)](https://www.nuget.org/packages/Ridge/) ![GitHub Repo stars](https://img.shields.io/github/stars/Melchy/Ridge?style=social) 2023-08-20 - 7 [TUnit](/docs/TUnit) [![Nuget](https://img.shields.io/nuget/dt/TUnit?label=TUnit)](https://www.nuget.org/packages/TUnit/) ![GitHub Repo stars](https://img.shields.io/github/stars/thomhurst/TUnit?style=social) 2025-11-08 + 7 [Rocks](/docs/Rocks) [![Nuget](https://img.shields.io/nuget/dt/Rocks?label=Rocks)](https://www.nuget.org/packages/Rocks/) ![GitHub Repo stars](https://img.shields.io/github/stars/JasonBock/Rocks?style=social) 2023-04-16 + + 8 [TUnit](/docs/TUnit) [![Nuget](https://img.shields.io/nuget/dt/TUnit?label=TUnit)](https://www.nuget.org/packages/TUnit/) ![GitHub Repo stars](https://img.shields.io/github/stars/thomhurst/TUnit?style=social) 2025-11-08 ### See category diff --git a/v2/rscg_examples_site/docs/RSCG-Examples/ErrorOrX.md b/v2/rscg_examples_site/docs/RSCG-Examples/ErrorOrX.md index c465edcd2..d478fb6c1 100644 --- a/v2/rscg_examples_site/docs/RSCG-Examples/ErrorOrX.md +++ b/v2/rscg_examples_site/docs/RSCG-Examples/ErrorOrX.md @@ -46,10 +46,10 @@ Alexander Nachtmanns ![Alt text](https://github.com/ANcpLua.png) ::: -### Original Readme +## Original Readme :::note -# ErrorOrX +### ErrorOrX [![NuGet](https://img.shields.io/nuget/v/ErrorOrX.Generators.svg)](https://www.nuget.org/packages/ErrorOrX.Generators/) [![NuGet Downloads](https://img.shields.io/nuget/dt/ErrorOrX.Generators.svg)](https://www.nuget.org/packages/ErrorOrX.Generators/) @@ -58,7 +58,7 @@ Alexander Nachtmanns Railway-Oriented Programming for .NET with source-generated ASP.NET Core Minimal API integration. Zero boilerplate, full Native AOT support. -## Features +###### Features - **Discriminated Unions** - `ErrorOr` represents success or a list of typed errors - **Fluent API** - Chain operations with `Then`, `Else`, `Match`, `Switch`, and `FailIf` @@ -72,12 +72,12 @@ Native AOT support. - **API Versioning** - Integrates with Asp.Versioning.Http for versioned endpoint groups - **41 Analyzers** - Real-time IDE feedback for route conflicts, binding errors, AOT compatibility -## What the Generator Produces +###### What the Generator Produces The source generator transforms your handler methods into complete ASP.NET Core Minimal API endpoints. You write the business logic, the generator handles everything else. -### Endpoint Wiring +######### Endpoint Wiring For each `[Get]`, `[Post]`, `[Put]`, `[Delete]`, `[Patch]` method: @@ -87,7 +87,7 @@ For each `[Get]`, `[Post]`, `[Put]`, `[Delete]`, `[Patch]` method: [See EndpointMetadataEmitter.cs](https://github.com/ANcpLua/ErrorOrX/src/ErrorOrX.Generators/Emitters/EndpointMetadataEmitter.cs) -### Parameter Binding +######### Parameter Binding Automatic inference based on type and HTTP method: @@ -101,7 +101,7 @@ Automatic inference based on type and HTTP method: [See BindingCodeEmitter.cs](https://github.com/ANcpLua/ErrorOrX/src/ErrorOrX.Generators/Emitters/BindingCodeEmitter.cs) -### Error-to-HTTP Mapping +######### Error-to-HTTP Mapping Converts `ErrorOr` errors to proper HTTP responses with [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) ProblemDetails: @@ -119,7 +119,7 @@ ProblemDetails: [See ErrorMapping.cs](https://github.com/ANcpLua/ErrorOrX/src/ErrorOrX.Generators/Models/ErrorMapping.cs) -### Request Validation +######### Request Validation Generated code validates before calling your handler: @@ -128,7 +128,7 @@ Generated code validates before calling your handler: - JSON deserialization (catches `JsonException`) - Content-Type checking (returns 415 for wrong type) -### OpenAPI Metadata +######### OpenAPI Metadata Full OpenAPI documentation without manual attributes: @@ -140,7 +140,7 @@ Full OpenAPI documentation without manual attributes: [See OpenApiTransformerGenerator.cs](https://github.com/ANcpLua/ErrorOrX/src/ErrorOrX.Generators/OpenApiTransformerGenerator.cs) -### Builder API +######### Builder API Fluent configuration following ASP.NET Core patterns: @@ -155,7 +155,7 @@ app.MapErrorOrEndpoints() .RequireRateLimiting("api"); // Global rate limit ``` -### Analyzers (38 Diagnostics) +######### Analyzers (38 Diagnostics) Real-time IDE feedback covering: @@ -171,7 +171,7 @@ Real-time IDE feedback covering: [See Descriptors.cs](https://github.com/ANcpLua/ErrorOrX/src/ErrorOrX.Generators/Analyzers/Descriptors.cs) -## Installation +###### Installation ```bash dotnet add package ErrorOrX.Generators @@ -179,7 +179,7 @@ dotnet add package ErrorOrX.Generators This package includes both the source generator and the `ErrorOrX` runtime library. -## Quick Start +###### Quick Start ```csharp // Program.cs @@ -208,7 +208,7 @@ public static class TodoApi } ``` -## Error Types +###### Error Types Create structured errors mapped to HTTP status codes: @@ -223,7 +223,7 @@ Error.Unexpected("Unknown", "An unexpected error occurred") // 500 Error.Custom(422, "Validation.Complex", "Complex validation failed") ``` -## Nullable-to-ErrorOr Extensions +###### Nullable-to-ErrorOr Extensions Convert nullable values to `ErrorOr` with auto-generated error codes: @@ -250,7 +250,7 @@ return value.OrError(() => BuildExpensiveError()); // Lazy evaluation | `.OrError(Error)` | Any | Any | Custom error | | `.OrError(Func)` | Any | Any | Lazy custom error | -## Fluent API +###### Fluent API Chain operations using railway-oriented programming patterns: @@ -275,7 +275,7 @@ GetUser(id).Switch( errors => Logger.LogError(errors.First().Description)); ``` -## Result Markers +###### Result Markers Use semantic markers for endpoints without response bodies: @@ -286,7 +286,7 @@ Result.Updated // 204 No Content Result.Deleted // 204 No Content ``` -## Interface Types with `[ReturnsError]` +###### Interface Types with `[ReturnsError]` Document possible errors on interface methods for OpenAPI generation: @@ -307,7 +307,7 @@ public static ErrorOr GetById(Guid id, ITodoService svc) => The generator reads `[ReturnsError]` attributes from interface/abstract methods to build the complete `Results<...>` union for OpenAPI documentation. -## Smart Parameter Binding +###### Smart Parameter Binding The generator automatically infers parameter sources: @@ -325,7 +325,7 @@ public static ErrorOr GetById( => svc.GetById(id).OrNotFound(); ``` -## Middleware Attributes +###### Middleware Attributes Standard ASP.NET Core attributes on your handler methods are translated to Minimal API fluent calls: @@ -343,7 +343,7 @@ public static ErrorOr CreateAdmin(CreateUserRequest req) \{ } // .CacheOutput(policy => policy.Expire(TimeSpan.FromSeconds(60))); ``` -## Native AOT +###### Native AOT Fully compatible with `PublishAot=true`. Create a `JsonSerializerContext` with your endpoint types: @@ -373,14 +373,14 @@ app.Run(); The `[JsonSourceGenerationOptions]` on your context controls serialization behavior (camelCase, null handling). The builder methods `WithCamelCase()` and `WithIgnoreNulls()` are only needed if you want to override at runtime. -## Packages +###### Packages | Package | Target | Description | |-----------------------|------------------|--------------------------------------| | `ErrorOrX.Generators` | `netstandard2.0` | Source generator (includes ErrorOrX) | | `ErrorOrX` | `net10.0` | Runtime library (auto-referenced) | -## Changelog +###### Changelog See [CHANGELOG.md](https://github.com/ANcpLua/ErrorOrX/CHANGELOG.md) for version history. diff --git a/v2/rscg_examples_site/docs/RSCG-Examples/KnockOff.md b/v2/rscg_examples_site/docs/RSCG-Examples/KnockOff.md new file mode 100644 index 000000000..59401b130 --- /dev/null +++ b/v2/rscg_examples_site/docs/RSCG-Examples/KnockOff.md @@ -0,0 +1,1048 @@ +--- +sidebar_position: 2550 +title: 255 - KnockOff +description: Generating test stubs with mocking for interfaces +slug: /KnockOff +--- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import TOCInline from '@theme/TOCInline'; +import SameCategory from '../Categories/_PrimitiveTests.mdx'; + +# KnockOff by Keith Voels + + + + +## NuGet / site data +[![Nuget](https://img.shields.io/nuget/dt/KnockOff?label=KnockOff)](https://www.nuget.org/packages/KnockOff/) +[![GitHub last commit](https://img.shields.io/github/last-commit/NeatooDotNet/KnockOff?label=updated)](https://github.com/NeatooDotNet/KnockOff) +![GitHub Repo stars](https://img.shields.io/github/stars/NeatooDotNet/KnockOff?style=social) + +## Details + +### Info +:::info + +Name: **KnockOff** + +A Roslyn Source Generator for creating unit test stubs. Unlike Moq's fluent runtime configuration, KnockOff uses partial classes for compile-time setup—trading flexibility for readability and performance. + +Author: Keith Voels + +NuGet: +*https://www.nuget.org/packages/KnockOff/* + + +You can find more details at https://github.com/NeatooDotNet/KnockOff + +Source: https://github.com/NeatooDotNet/KnockOff + +::: + +### Author +:::note +Keith Voels +![Alt text](https://github.com/NeatooDotNet.png) +::: + +## Original Readme +:::note + +### KnockOff + +A .NET mocking library that lets you define reusable stub classes — with full mocking capabilities built in. + +Define your test double once. Reuse it across your test project. Customize it per-test with Return, Call, Verify, and When chains. No more copying mock setups between tests or maintaining shared factory methods full of `Arg.Any<>()`. + +Powered by Roslyn source generation for [tighter type safety](https://github.com/NeatooDotNet/KnockOff/docs/type-safety.md) — more issues surface as compile errors instead of runtime surprises. + +Claude Code was used to write this library. Skip to more [AI discussion](#ai). + +[![NuGet](https://img.shields.io/nuget/v/KnockOff.svg)](https://www.nuget.org/packages/KnockOff/) +[![Build Status](https://github.com/NeatooDotNet/KnockOff/workflows/Build,%20Test%20&%20Publish/badge.svg)](https://github.com/NeatooDotNet/KnockOff/actions) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +###### KnockOff Stub + +There are [9 patterns](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md) total, including a [standard fluent mocking approach](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md#inline-interface-pattern) with inline stubs. But reusable stub classes are where KnockOff stands apart: + + +```cs +[KnockOff] +public partial class MyRepoStub(List Users) : IMyRepo +{ + protected override User? GetUser_(int id) + { + return Users.Single(u => u.Id == id); + } + + protected override void Update_(User user) + { + Assert.Contains(user, Users); + } +} +``` + + +- **`[KnockOff]` + `partial class`** — KnockOff generates a base class that implements every member of `IMyRepo`. Your stub is a real class — define it once, reuse it across your entire test project. Pass it around, register it in DI, share it between test fixtures. +- **Constructor parameters** — `List Users` is a primary constructor. Test data flows in naturally, just like any other C# class. +- **Overrides are optional** — `GetUser_` and `Update_` override the generated defaults. Only override what you need — everything else still works with [Return/Call](https://github.com/NeatooDotNet/KnockOff/docs/guides/methods.md), [Return(value)](https://github.com/NeatooDotNet/KnockOff/docs/reference/interceptor-api.md), or [When chains](https://github.com/NeatooDotNet/KnockOff/docs/guides/parameter-matching.md). +- **Tighter type safety** — Every Return, Call, and When call is complete in a single step — no forgotten `.Returns()` that [silently breaks at runtime](https://github.com/NeatooDotNet/KnockOff/docs/type-safety.md). No manual `` type parameters that can drift. [Details →](https://github.com/NeatooDotNet/KnockOff/docs/type-safety.md) + +This stub is also a full mock. It has [Verify](https://github.com/NeatooDotNet/KnockOff/docs/guides/verification.md), [Strict mode](https://github.com/NeatooDotNet/KnockOff/docs/guides/strict-mode.md), [Async](https://github.com/NeatooDotNet/KnockOff/docs/guides/async-patterns.md), and [Source Delegation](https://github.com/NeatooDotNet/KnockOff/docs/guides/source-delegation.md) — all on the same reusable class. + + +###### Why I Wrote KnockOff + +I often wanted to reuse my mocks. +Especially in my integration test library where I may even register my mocks. +I found myself either copying my mock definitions code or creating shared methods like this: + +**NSubstitute:** + +```cs +public static IMyRepo NSubstituteMock(List users) +{ + var myRepoMock = Substitute.For(); + + // Setup: configure GetUser to look up from the list based on id + myRepoMock.GetUser(Arg.Any()) + .Returns(callInfo => users.SingleOrDefault(u => u.Id == callInfo.Arg())); + + // Setup: configure Update to assert user exists in list + myRepoMock.When(x => x.Update(Arg.Any())) + .Do(callInfo => Assert.Contains(callInfo.Arg(), users)); + + return myRepoMock; +} +``` + + +Here's another [example from PowerToys](https://github.com/microsoft/PowerToys/blob/main/src/settings-ui/Settings.UI.UnitTests/Mocks/ISettingsUtilsMocks.cs). + +But I find that hard to read and unintuitive. Also, my shared methods accumulated extra parameters for variations across different tests. + + +###### So I Created KnockOff + +You can create a stub to implement [interfaces](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md) or non-sealed [classes](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md) with virtual methods. +Yet, you can still customize the stub per test. +All while having the features you would expect with a full mocking library. + +With the stub above, your tests are: + + +```cs +var myRepoKO = new MyRepoStub([new User \{ Id = 1 }, new User \{ Id = 2 }]); +var userDomainModel = new UserDomainModel(myRepoKO); + +Assert.True(userDomainModel.Fetch(1)); + +// I have Verify on my Stub! +myRepoKO.GetUser.Verify(Called.Once); +``` + + +Need different behavior for a specific test? Override with Return/Call: + + +```cs +var user1 = new User \{ Id = 1 }; // Ignored do to per-test configuration +var myRepoKO = new MyRepoStub([user1]); +var userDomainModel = new UserDomainModel(myRepoKO); + +var user2 = new User \{ Id = 2 }; + +// When and Return overrides the stub methods +myRepoKO.GetUser.When(2).Return(user2).Verifiable(); +myRepoKO.Update.Call(u => Assert.Same(u, user2)).Verifiable(); + +userDomainModel.Fetch(2); +userDomainModel.Update(); + +myRepoKO.Verify(); +``` + + +**Now I have my stubs and mocks in one!** + +--- + +###### What Sets KnockOff Apart + +- **[Reusable stub classes](https://github.com/NeatooDotNet/KnockOff/docs/guides/reusable-stubs.md)** — Define once, customize per-test. Your stub is a real class — pass it through constructors, register it in DI. +- **[Source delegation](https://github.com/NeatooDotNet/KnockOff/docs/guides/source-delegation.md)** — Delegate to a real implementation, override only specific methods. No equivalent in Moq or NSubstitute. +- **[Protected methods](https://github.com/NeatooDotNet/KnockOff/docs/guides/protected-methods.md)** — Same `Return`/`Call`/`Verify` API, fully typed. No string-based names, no manual subclasses. +- **[Ref/out parameters](https://github.com/NeatooDotNet/KnockOff/docs/guides/ref-out-parameters.md)** — Natural lambda syntax with `ref`/`out` keywords. No special matchers or index-based access. +- **[Multiple interfaces](https://github.com/NeatooDotNet/KnockOff/docs/guides/multiple-interfaces.md)** — Unified interceptors on one stub. No `.As()` references or casting. +- **[Tighter type safety](https://github.com/NeatooDotNet/KnockOff/docs/type-safety.md)** — Each Return/Call/When call is complete in one step — no forgotten `.Returns()` that silently breaks at runtime. +- **[Parameter matching](https://github.com/NeatooDotNet/KnockOff/docs/guides/parameter-matching-comparison.md)** — `Return((a, b) => a > 0 ? 100 : 0)` — standard C# conditionals instead of `Arg.Is<>` or `It.Is<>` per parameter. +- **Built-in argument capture** — `LastArg`, `LastArgs`, `LastSetValue`, `LastSetEntry` — no manual `Arg.Do<>` or `Callback<>` setup. +- **Event verification** — `VerifyAdd()` / `VerifyRemove()` / `HasSubscribers` — not available in Moq or NSubstitute. +- **Explicit Get/Set verification** — `VerifyGet(Called)` / `VerifySet(Called)` for properties and indexers. +- **Stubbing concrete classes** — Override virtual methods on non-sealed classes with the same API. + +--- + +###### Quick Start + +######### Install + +```bash +dotnet add package KnockOff +``` + +######### Create a Stub + + +```cs +public interface IQuickStartRepo +{ + User? GetUser(int id); +} + +[KnockOff] +public partial class QuickStartRepoStub : IQuickStartRepo \{ } + +public class QuickStartCreateStubTests +{ + [Fact] + public void CreateStub_IsReady() + { + var stub = new QuickStartRepoStub(); + + IQuickStartRepo repository = stub; + Assert.NotNull(repository); + } +} +``` + + +######### Configure and Verify + + +```cs +[Fact] +public void ConfigureStub_WithReturn() +{ + var stub = new QuickStartRepoStub(); + + stub.GetUser.Return((id) => new User \{ Id = id, Name = "Test User" }); + + IQuickStartRepo repository = stub; + var user = repository.GetUser(42); + + Assert.NotNull(user); + Assert.Equal(42, user.Id); + Assert.Equal("Test User", user.Name); +} +``` + + + +```cs +[Fact] +public void VerifyCalls_WithVerifiable() +{ + var stub = new QuickStartRepoStub(); + stub.GetUser.Return((id) => new User \{ Id = id, Name = "Test" }).Verifiable(); + + IQuickStartRepo repository = stub; + + var user = repository.GetUser(42); + + // Verify() checks all members marked with .Verifiable() + stub.Verify(); +} +``` + + +--- + +###### The Difference + +**Moq:** +```cs +mock.Setup(x => x.GetUser(It.Is(id => id > 0))) + .Returns(id => new User \{ Id = id }); +``` + +**NSubstitute:** + +```cs +var repo = Substitute.For(); +repo.GetUser(Arg.Is(id => id > 0)).Returns(x => new User \{ Id = x.Arg() }); +``` + + +**KnockOff:** + +```cs +var stub = new CompareUserRepoStub(); +stub.GetUser.Return((id) => id > 0 ? new User \{ Id = id \} : null); +``` + + +No `It.Is<>()`. No `Arg.Is<>()`. No `x.Arg()`. The parameter is just `id`. + +--- + +For side-by-side comparison tables (methods, properties, events, delegates, indexers), see the [complete comparison guide](https://github.com/NeatooDotNet/KnockOff/docs/comparison.md). + +--- + +###### Argument Matching + +**Moq:** +```cs +// Moq - It.Is per parameter +mock.Setup(x => x.Add(It.Is(a => a > 0), It.IsAny())).Returns(100); +``` + +**NSubstitute:** + +```cs +// NSubstitute - Arg.Is per parameter (permanent matchers) +calc.Add(Arg.Is(a => a > 0), Arg.Any()).Returns(100); +``` + + +**KnockOff:** + +```cs +// KnockOff - Returns with conditional (permanent, matches all calls) +stub.Add.Return((a, b) => a > 0 ? 100 : 0); +``` + + + +```cs +// KnockOff - When() for sequential matching (first match returns 100, then falls through) +stub.Add.When((a, b) => a > 0).Return(100).ThenCall((a, b) => a + b); +``` + + +**Multiple specific values:** + +**Moq:** +```cs +mock.Setup(x => x.Add(1, 2)).Returns(100); +mock.Setup(x => x.Add(3, 4)).Returns(200); +``` + + +```cs +// Multiple specific values +calc.Add(1, 2).Returns(100); +calc.Add(3, 4).Returns(200); +``` + + + +```cs +stub.Add.When(1, 2).Return(100); +stub.Add.When(3, 4).Return(200); +``` + + +**Note:** Moq and NSubstitute matchers are permanent -- they match all qualifying calls. KnockOff's `When()` is sequential -- matchers are consumed in order. Use `Return(callback)` with conditionals for permanent matching behavior. + +######### Argument Capture + +**Moq:** +```cs +// Moq - requires Callback setup +int capturedA = 0, capturedB = 0; +mock.Setup(x => x.Add(It.IsAny(), It.IsAny())) + .Callback((a, b) => \{ capturedA = a; capturedB = b; }); +mock.Object.Add(1, 2); +``` + +**NSubstitute:** + +```cs +// NSubstitute - requires Arg.Do in setup +int capturedA = 0, capturedB = 0; +calc.Add(Arg.Do(x => capturedA = x), Arg.Do(x => capturedB = x)); +calc.Add(1, 2); +``` + + +**KnockOff:** + +```cs +// KnockOff - built-in, no pre-setup +var tracking = stub.Add.Return((a, b) => a + b); +ICalculator calc = stub; +calc.Add(1, 2); +var (a, b) = tracking.LastArgs; // Named tuple: a = 1, b = 2 +``` + + +For full comparisons of properties, events, delegates, and indexers, see the [complete comparison guide](https://github.com/NeatooDotNet/KnockOff/docs/comparison.md). + +--- + +###### Method Overload Resolution + +**The Problem:** When an interface has overloaded methods with the same parameter count but different types: + + +```cs +public interface IFormatter +{ + string Format(string input, bool uppercase); + string Format(string input, int maxLength); +} +``` + + +######### Any-Value Matching + +**Moq:** +```cs +// It.IsAny() required - compiler needs the types to resolve overload +mock.Setup(x => x.Format(It.IsAny(), It.IsAny())).Returns("bool overload"); +mock.Setup(x => x.Format(It.IsAny(), It.IsAny())).Returns("int overload"); +``` + +**NSubstitute:** + +```cs +// Arg.Any() required - compiler needs the types to resolve overload +formatter.Format(Arg.Any(), Arg.Any()).Returns("bool overload"); +formatter.Format(Arg.Any(), Arg.Any()).Returns("int overload"); +``` + + +**KnockOff:** + +```cs +// Explicit parameter types resolve the overload - standard C# syntax +stub.Format.Return((string input, bool uppercase) => "bool overload"); +stub.Format.Return((string input, int maxLength) => "int overload"); +``` + + +######### Specific-Value Matching + +**NSubstitute:** + +```cs +// Specific value matching - literals work when all args are specific +formatter.Format("test", true).Returns("UPPERCASE"); +formatter.Format("test", 10).Returns("truncated"); +``` + + +**KnockOff:** + +```cs +// Specific value matching - parameter types resolve the overload +stub.Format.When("test", true).Return("UPPERCASE"); +stub.Format.When("test", 10).Return("truncated"); +``` + + +######### Argument Access + +**Moq:** +```cs +// To use argument values, extract via Returns: +mock.Setup(x => x.Format(It.IsAny(), It.IsAny())) + .Returns((input, uppercase) => uppercase ? input.ToUpper() : input); +``` + +**NSubstitute:** + +```cs +// To use argument values, extract from CallInfo: +formatter.Format(Arg.Any(), Arg.Any()) + .Returns(x => x.ArgAt(1) ? x.ArgAt(0).ToUpper() : x.ArgAt(0)); +``` + + +**KnockOff:** + +```cs +// Arguments are directly available with names and types: +stub.Format.Return((string input, bool uppercase) => uppercase ? input.ToUpper() : input); +``` + + +**The Difference:** +- Moq: `It.IsAny()` + `.Returns((input, uppercase) => ...)` to match any value and access arguments +- NSubstitute: `Arg.Any()` + `x.ArgAt(1)` to match any value and access arguments +- KnockOff: `(string input, bool uppercase)` - standard C# lambda with named, typed parameters + +--- + +###### Three Stub Patterns + +KnockOff supports [9 patterns](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md) total. Here are the three most common: + +**[Standalone](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md#standalone-pattern)** - Reusable across your project: + +```cs +[KnockOff] +public partial class ReadmeStandaloneStub : IUserRepo \{ } +``` + + +**[Inline Interface](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md#inline-interface-pattern)** - Test-local stubs: + +```cs +[Fact] +public void InlineInterface_Pattern() +{ + var stub = new Stubs.IUserRepo(); + stub.GetUser.Return((id) => new User \{ Id = id }); + + IUserRepo repo = stub; + Assert.NotNull(repo.GetUser(1)); +} +``` + + +**[Inline Class](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md#inline-class-pattern)** - Stub virtual members: + +```cs +[Fact] +public void InlineClass_Pattern() +{ + var stub = new Stubs.MyService(); + stub.GetUser.Return((id) => new User \{ Id = id }); + + MyService service = stub.Object; + Assert.NotNull(service.GetUser(1)); +} +``` + + +--- + +###### Roslyn Source Generation + +KnockOff uses Roslyn source generation, which means: + +- No more `Arg.Any<>()`. No more `It.IsAny<>()`. Just write C# +- If the method signature changes you get a compile error +- There's a small performance gain but honestly it's negligible + +Source generation opens doors beyond traditional mocking — I've already added [9 patterns](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md) and features like [Source Delegation](https://github.com/NeatooDotNet/KnockOff/docs/guides/source-delegation.md), with more ideas to come. + +**What other ideas do you have?** Open a [discussion](https://github.com/NeatooDotNet/KnockOff/discussions). + + +###### AI + +This is an idea I've had for years but never took the time to implement. With my ideas and guidance, Claude Code has written the entirety of this library — the Roslyn source generator, the runtime library, the tests, and the documentation. + +Source generation turned out to be a great fit for AI code generation. The work is highly patterned: analyze an interface, generate code for each member, handle edge cases across 9 patterns and 4 member types. That's exactly the kind of systematic, repetitive-but-varied work where AI excels. I designed the API and patterns; Claude Code implemented them across every combination. + +######### Claude Code Skill + +KnockOff includes a [Claude Code skill](https://github.com/NeatooDotNet/KnockOff/skillsknockoff/) that teaches Claude how to use the library. Copy the `skills/knockoff/` directory into your project and Claude Code will know how to create stubs, configure behavior, write tests with KnockOff, and migrate from Moq — without you explaining the API. + +The skill includes slash commands: +- **`/knockoff:create-stub`** — Create a new stub class with the pattern of your choice +- **`/knockoff:migrate-from-moq`** — Convert existing Moq tests to KnockOff +- **`/knockoff:troubleshoot`** — Diagnose and fix common KnockOff issues + +--- + +###### Documentation + +- **[Getting Started](https://github.com/NeatooDotNet/KnockOff/docs/getting-started.md)** - Installation and first stub +- **[Stub Patterns](https://github.com/NeatooDotNet/KnockOff/docs/guides/stub-patterns.md)** - Standalone, inline interface, inline class +- **[Interceptor API](https://github.com/NeatooDotNet/KnockOff/docs/reference/interceptor-api.md)** - Complete `Returns`, `Execute`, `Get`, `Set` reference +- **[Source Delegation](https://github.com/NeatooDotNet/KnockOff/docs/guides/source-delegation.md)** - Delegate to real implementations +- **[Full Comparison Guide](https://github.com/NeatooDotNet/KnockOff/docs/comparison.md)** - Properties, events, delegates, indexers vs Moq and NSubstitute +- **[Migration from Moq](https://github.com/NeatooDotNet/KnockOff/docs/migration/from-moq.md)** - Step-by-step migration guide +- **[Migration from NSubstitute](https://github.com/NeatooDotNet/KnockOff/docs/migration/from-nsubstitute.md)** - Comparison and migration guide + +--- + +###### License + +MIT License. See [LICENSE](https://github.com/NeatooDotNet/KnockOff/LICENSE) for details. + +--- + +###### Contributing + +Contributions welcome! See [CONTRIBUTING.md](https://github.com/NeatooDotNet/KnockOff/CONTRIBUTING.md) for guidelines. + +- **Issues**: [GitHub Issues](https://github.com/NeatooDotNet/KnockOff/issues) +- **Pull Requests**: Bug fixes, features, documentation +- **Discussions**: [GitHub Discussions](https://github.com/NeatooDotNet/KnockOff/discussions) + + +::: + +### About +:::note + +Generating test stubs with mocking for interfaces + + +::: + +## How to use + +### Example (source csproj, source files) + + + + + +This is the CSharp Project that references **KnockOff** +```xml showLineNumbers {13} + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + true + $(BaseIntermediateOutputPath)\GX + + + + +``` + + + + + + This is the use of **KnockOff** in *IMyClock.cs* + +```csharp showLineNumbers +namespace MockData; + +public interface IMyClock +{ + public DateTime GetNow(); + public DateTime GetUtcNow(); +} +``` + + + + + This is the use of **KnockOff** in *TestClock.cs* + +```csharp showLineNumbers + +using KnockOff; + +namespace TestClock; + +[KnockOff] +public partial class QuickStartRepoStub : IMyClock \{ } + + +[TestClass] +public class TestClock +{ + [TestMethod] + public void TestMyClock() + { + var expectations = new QuickStartRepoStub(); + expectations.GetNow.Return(DateTime.Now.AddYears(-1)); + + IMyClock mock = expectations; + var data= mock.GetNow(); + Assert.AreEqual(DateTime.Now.Year -1, data.Year); + expectations.Verify(); + } +} + + + +``` + + + + +### Generated Files + +Those are taken from $(BaseIntermediateOutputPath)\GX + + + + +```csharp showLineNumbers +// +#nullable enable + +namespace TestClock; + +public class QuickStartRepoStubBase +{ + /// Override to provide default implementation for global::MockData.IMyClock.GetNow. + protected virtual global::System.DateTime GetNow_() => default!; + + /// Override to provide default implementation for global::MockData.IMyClock.GetUtcNow. + protected virtual global::System.DateTime GetUtcNow_() => default!; + +} + +``` + + + + +```csharp showLineNumbers +// +#nullable enable + +using System.Linq; + +namespace TestClock; + +partial class QuickStartRepoStub : QuickStartRepoStubBase, global::MockData.IMyClock, global::KnockOff.IKnockOffStub +{ + /// Tracks and configures behavior for GetNow. + public sealed class GetNowInterceptor : global::KnockOff.Interceptors.MethodInterceptorBase + { + /// Source object to delegate to when no callback is configured. + internal global::MockData.IMyClock? _source; + + /// Delegate for GetNow. + public delegate global::System.DateTime GetNowDelegate(); + + public GetNowInterceptor() : base("GetNow") \{ } + + protected override global::System.DateTime InvokeDelegate(GetNowDelegate del, global::KnockOff.Unit args) => del(); + protected override GetNowDelegate CreateValueDelegate(global::System.DateTime value) => () => value; + protected override void RecordArgs(global::KnockOff.Unit args, MethodCallBuilderBase tracking) \{ } + protected override void RecordUnconfiguredArgs(global::KnockOff.Unit args) \{ } + + /// Configures callback that repeats indefinitely. Returns builder for sequence chaining. + public MethodCallBuilderImpl Return(GetNowDelegate callback) + { + var builder = new MethodCallBuilderImpl(this); + SetupReturnCallback(callback, builder); + return builder; + } + + /// Configures return value that repeats indefinitely. Returns builder for sequence chaining. + public MethodCallBuilderImpl Return(global::System.DateTime value) + { + var builder = new MethodCallBuilderImpl(this); + SetupReturnValue(value, builder); + return builder; + } + + /// Configures sequence of return values. Each value returned once, last repeats. + public ReturnMethodSequenceBase Return(global::System.DateTime first, params global::System.DateTime[] rest) + { + var builder = Return(() => first); + if (rest.Length == 0) + { + return builder.ThenReturn(first); + } + var seq = builder.ThenReturn(rest[0]); + for (int i = 1; i < rest.Length; i++) + { + seq.ThenReturn(rest[i]); + } + return seq; + } + + /// Invokes the configured callback. Called by explicit interface implementation. + internal global::System.DateTime Invoke(bool strict) + { + var (handled, result) = RunPriorityChain(default); + if (handled) return result; + _unconfiguredCallCount++; + RecordUnconfiguredArgs(default); + var (seqHandled, seqResult) = HandleNonVoidSequenceExhaustedRepeat(strict, default); + if (seqHandled) return seqResult; + #pragma warning disable CS8601, SYSLIB0050 + if (_source is \{ \} src) return src.GetNow(); + #pragma warning restore CS8601, SYSLIB0050 + if (strict) throw global::KnockOff.StubException.NotConfigured("", "GetNow"); + return default!; + } + + /// Resets tracking state but preserves configuration and verifiable marking. + public override void Reset() + { + base.Reset(); + _source = null; + } + + /// Builder for callback registration. Supports tracking and lazy elevation to sequence. + public sealed class MethodCallBuilderImpl : ReturnMethodCallBuilderBase, global::KnockOff.IMethodReturnBuilder + { + private readonly GetNowInterceptor _typedInterceptor; + + public MethodCallBuilderImpl(GetNowInterceptor interceptor) : base(interceptor) + { + _typedInterceptor = interceptor; + } + + + public override void Reset() => base.Reset(); + + /// Elevates to sequence mode and adds another callback. Returns sequence for further chaining. + public ReturnMethodSequenceBase ThenReturn(GetNowDelegate callback) + { + return ThenReturnBase(callback); + } + + /// Elevates to sequence mode and adds a value. Returns sequence for further chaining. + public ReturnMethodSequenceBase ThenReturn(global::System.DateTime value) => ThenReturn(() => value); + + /// Adds multiple values to the sequence. Each value returned once. + public ReturnMethodSequenceBase ThenReturn(params global::System.DateTime[] values) + { + if (values.Length == 0) \{ ElevateToSequenceBase(); return new ReturnMethodSequenceBase(_typedInterceptor, CreateNextReturnBuilder); } + var seq = ThenReturn(values[0]); + for (int i = 1; i < values.Length; i++) seq.ThenReturn(values[i]); + return seq; + } + + /// Marks for verification by Stub.Verify(). + public MethodCallBuilderImpl Verifiable() \{ VerifiableBase(); return this; } + /// Marks for verification by Stub.Verify() with Called constraint. + public MethodCallBuilderImpl Verifiable(global::KnockOff.Called times) \{ VerifiableBase(times); return this; } + + protected override ReturnMethodCallBuilderBase CreateNextReturnBuilder() => new MethodCallBuilderImpl(_typedInterceptor); + + global::KnockOff.IMethodTracking global::KnockOff.IMethodTracking.Verifiable() => Verifiable(); + global::KnockOff.IMethodTracking global::KnockOff.IMethodTracking.Verifiable(global::KnockOff.Called times) => Verifiable(times); + global::KnockOff.IMethodReturnBuilder global::KnockOff.IMethodReturnBuilder.Verifiable() => Verifiable(); + global::KnockOff.IMethodReturnBuilder global::KnockOff.IMethodReturnBuilder.Verifiable(global::KnockOff.Called times) => Verifiable(times); + global::KnockOff.IMethodReturnSequence global::KnockOff.IMethodReturnBuilder.ThenReturn(GetNowDelegate callback) => ThenReturn(callback); + } + + } + + /// Tracks and configures behavior for GetUtcNow. + public sealed class GetUtcNowInterceptor : global::KnockOff.Interceptors.MethodInterceptorBase + { + /// Source object to delegate to when no callback is configured. + internal global::MockData.IMyClock? _source; + + /// Delegate for GetUtcNow. + public delegate global::System.DateTime GetUtcNowDelegate(); + + public GetUtcNowInterceptor() : base("GetUtcNow") \{ } + + protected override global::System.DateTime InvokeDelegate(GetUtcNowDelegate del, global::KnockOff.Unit args) => del(); + protected override GetUtcNowDelegate CreateValueDelegate(global::System.DateTime value) => () => value; + protected override void RecordArgs(global::KnockOff.Unit args, MethodCallBuilderBase tracking) \{ } + protected override void RecordUnconfiguredArgs(global::KnockOff.Unit args) \{ } + + /// Configures callback that repeats indefinitely. Returns builder for sequence chaining. + public MethodCallBuilderImpl Return(GetUtcNowDelegate callback) + { + var builder = new MethodCallBuilderImpl(this); + SetupReturnCallback(callback, builder); + return builder; + } + + /// Configures return value that repeats indefinitely. Returns builder for sequence chaining. + public MethodCallBuilderImpl Return(global::System.DateTime value) + { + var builder = new MethodCallBuilderImpl(this); + SetupReturnValue(value, builder); + return builder; + } + + /// Configures sequence of return values. Each value returned once, last repeats. + public ReturnMethodSequenceBase Return(global::System.DateTime first, params global::System.DateTime[] rest) + { + var builder = Return(() => first); + if (rest.Length == 0) + { + return builder.ThenReturn(first); + } + var seq = builder.ThenReturn(rest[0]); + for (int i = 1; i < rest.Length; i++) + { + seq.ThenReturn(rest[i]); + } + return seq; + } + + /// Invokes the configured callback. Called by explicit interface implementation. + internal global::System.DateTime Invoke(bool strict) + { + var (handled, result) = RunPriorityChain(default); + if (handled) return result; + _unconfiguredCallCount++; + RecordUnconfiguredArgs(default); + var (seqHandled, seqResult) = HandleNonVoidSequenceExhaustedRepeat(strict, default); + if (seqHandled) return seqResult; + #pragma warning disable CS8601, SYSLIB0050 + if (_source is \{ \} src) return src.GetUtcNow(); + #pragma warning restore CS8601, SYSLIB0050 + if (strict) throw global::KnockOff.StubException.NotConfigured("", "GetUtcNow"); + return default!; + } + + /// Resets tracking state but preserves configuration and verifiable marking. + public override void Reset() + { + base.Reset(); + _source = null; + } + + /// Builder for callback registration. Supports tracking and lazy elevation to sequence. + public sealed class MethodCallBuilderImpl : ReturnMethodCallBuilderBase, global::KnockOff.IMethodReturnBuilder + { + private readonly GetUtcNowInterceptor _typedInterceptor; + + public MethodCallBuilderImpl(GetUtcNowInterceptor interceptor) : base(interceptor) + { + _typedInterceptor = interceptor; + } + + + public override void Reset() => base.Reset(); + + /// Elevates to sequence mode and adds another callback. Returns sequence for further chaining. + public ReturnMethodSequenceBase ThenReturn(GetUtcNowDelegate callback) + { + return ThenReturnBase(callback); + } + + /// Elevates to sequence mode and adds a value. Returns sequence for further chaining. + public ReturnMethodSequenceBase ThenReturn(global::System.DateTime value) => ThenReturn(() => value); + + /// Adds multiple values to the sequence. Each value returned once. + public ReturnMethodSequenceBase ThenReturn(params global::System.DateTime[] values) + { + if (values.Length == 0) \{ ElevateToSequenceBase(); return new ReturnMethodSequenceBase(_typedInterceptor, CreateNextReturnBuilder); } + var seq = ThenReturn(values[0]); + for (int i = 1; i < values.Length; i++) seq.ThenReturn(values[i]); + return seq; + } + + /// Marks for verification by Stub.Verify(). + public MethodCallBuilderImpl Verifiable() \{ VerifiableBase(); return this; } + /// Marks for verification by Stub.Verify() with Called constraint. + public MethodCallBuilderImpl Verifiable(global::KnockOff.Called times) \{ VerifiableBase(times); return this; } + + protected override ReturnMethodCallBuilderBase CreateNextReturnBuilder() => new MethodCallBuilderImpl(_typedInterceptor); + + global::KnockOff.IMethodTracking global::KnockOff.IMethodTracking.Verifiable() => Verifiable(); + global::KnockOff.IMethodTracking global::KnockOff.IMethodTracking.Verifiable(global::KnockOff.Called times) => Verifiable(times); + global::KnockOff.IMethodReturnBuilder global::KnockOff.IMethodReturnBuilder.Verifiable() => Verifiable(); + global::KnockOff.IMethodReturnBuilder global::KnockOff.IMethodReturnBuilder.Verifiable(global::KnockOff.Called times) => Verifiable(times); + global::KnockOff.IMethodReturnSequence global::KnockOff.IMethodReturnBuilder.ThenReturn(GetUtcNowDelegate callback) => ThenReturn(callback); + } + + } + + /// Interceptor for GetNow. + public GetNowInterceptor GetNow \{ get; \} = new(); + + /// Interceptor for GetUtcNow. + public GetUtcNowInterceptor GetUtcNow \{ get; \} = new(); + + /// When true, throws StubException for unconfigured member access. + public bool Strict \{ get; set; \} = false; + + /// The global::MockData.IMyClock instance. Use for passing to code expecting the interface. + public global::MockData.IMyClock Object => this; + + /// Verifies all members marked with .Verifiable() were invoked as expected. Throws VerificationException with all failures if any fail. + public void Verify() + { + var failures = new global::System.Collections.Generic.List(); + + if (GetNow.CheckVerification() is \{ \} getnowFailure) failures.Add(getnowFailure); + if (GetUtcNow.CheckVerification() is \{ \} getutcnowFailure) failures.Add(getutcnowFailure); + + if (failures.Count > 0) + throw new global::KnockOff.VerificationException(failures); + } + + /// Verifies ALL configured members were invoked at least once. Throws VerificationException with all failures if any fail. + public void VerifyAll() + { + var failures = new global::System.Collections.Generic.List(); + + if (GetNow.CheckVerificationAll() is \{ \} getnowFailure) failures.Add(getnowFailure); + if (GetUtcNow.CheckVerificationAll() is \{ \} getutcnowFailure) failures.Add(getutcnowFailure); + + if (failures.Count > 0) + throw new global::KnockOff.VerificationException(failures); + } + + // Source(T) methods for interface delegation + + /// Delegates unconfigured member access to the provided source object (global::MockData.IMyClock). + /// The source to delegate to, or null to clear. + public void Source(global::MockData.IMyClock? source) + { + GetNow._source = source; + GetUtcNow._source = source; + } + + global::System.DateTime global::MockData.IMyClock.GetNow() + { + return GetNow.Invoke(Strict); + } + + global::System.DateTime global::MockData.IMyClock.GetUtcNow() + { + return GetUtcNow.Invoke(Strict); + } + +} + +``` + + + + +## Useful + +### Download Example (.NET C#) + +:::tip + +[Download Example project KnockOff ](/sources/KnockOff.zip) + +::: + + +### Share KnockOff + + + +https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff + + + diff --git a/v2/rscg_examples_site/docs/RSCG-Examples/index.md b/v2/rscg_examples_site/docs/RSCG-Examples/index.md index 7f645cfd2..dac55d475 100644 --- a/v2/rscg_examples_site/docs/RSCG-Examples/index.md +++ b/v2/rscg_examples_site/docs/RSCG-Examples/index.md @@ -1,7 +1,7 @@ --- sidebar_position: 30 -title: 254 RSCG list by category -description: 254 RSCG list by category +title: 255 RSCG list by category +description: 255 RSCG list by category slug: /rscg-examples --- @@ -1522,7 +1522,7 @@ import DocCardList from '@theme/DocCardList'; ## Tests
- Expand Tests =>examples:7 + Expand Tests =>examples:8 @@ -1558,6 +1558,11 @@ import DocCardList from '@theme/DocCardList'; [Imposter](/docs/Imposter) + + + +[KnockOff](/docs/KnockOff) +
@@ -2100,6 +2105,8 @@ flowchart LR; Tests--> Imposter((Imposter)) + Tests--> KnockOff((KnockOff)) + Validator--> validly((validly)) WinAPI--> Com((Com)) diff --git a/v2/rscg_examples_site/docs/about.md b/v2/rscg_examples_site/docs/about.md index 4ad1df422..b81a91fe8 100644 --- a/v2/rscg_examples_site/docs/about.md +++ b/v2/rscg_examples_site/docs/about.md @@ -6,7 +6,7 @@ title: About ## Content You will find here code examples -of 254 Roslyn Source Code Generator (RSCG) +of 255 Roslyn Source Code Generator (RSCG) that can be useful for you. That means, you will write more elegant and concise code - even if the generators code is not always nice to look. ## Are those examples ready for production? diff --git a/v2/rscg_examples_site/docs/indexRSCG.md b/v2/rscg_examples_site/docs/indexRSCG.md index 16f185bd3..539881405 100644 --- a/v2/rscg_examples_site/docs/indexRSCG.md +++ b/v2/rscg_examples_site/docs/indexRSCG.md @@ -7,9 +7,9 @@ slug: /List-of-RSCG import useBaseUrl from '@docusaurus/useBaseUrl'; -## 254 RSCG with examples in descending chronological order +## 255 RSCG with examples in descending chronological order -This is the list of 254 ( 16 from Microsoft) RSCG with examples +This is the list of 255 ( 16 from Microsoft) RSCG with examples [See by category](/docs/rscg-examples) [See as json](/exports/RSCG.json) [See as Excel](/exports/RSCG.xlsx) @@ -20,6 +20,7 @@ This is the list of 254 ( 16 from Microsoft) RSCG with examples | No | Name | Date | Category | | --------- | ----- | ---- | -------- | +|255| [KnockOff by Keith Voels ](/docs/KnockOff)|2026-02-13 => 13 February 2026 | [Tests](/docs/Categories/Tests) | |254| [ErrorOrX by Alexander Nachtmanns ](/docs/ErrorOrX)|2026-02-02 => 02 February 2026 | [API](/docs/Categories/API) | |253| [FastCloner by Matěj Štágl ](/docs/FastCloner)|2026-02-01 => 01 February 2026 | [Clone](/docs/Categories/Clone) | |252| [RSCG_idempotency by Ignat Andrei ](/docs/RSCG_idempotency)|2026-01-28 => 28 January 2026 | [Idempotency](/docs/Categories/Idempotency) | diff --git a/v2/rscg_examples_site/src/components/HomepageFeatures/index.js b/v2/rscg_examples_site/src/components/HomepageFeatures/index.js index 7427a13c4..a4510b61c 100644 --- a/v2/rscg_examples_site/src/components/HomepageFeatures/index.js +++ b/v2/rscg_examples_site/src/components/HomepageFeatures/index.js @@ -4,7 +4,7 @@ import styles from './styles.module.css'; const FeatureList = [ { -title: '254 Examples (16 from MSFT)', +title: '255 Examples (16 from MSFT)', Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, description: ( <> diff --git a/v2/rscg_examples_site/static/exports/RSCG.json b/v2/rscg_examples_site/static/exports/RSCG.json index abef16315..39a8a25cc 100644 --- a/v2/rscg_examples_site/static/exports/RSCG.json +++ b/v2/rscg_examples_site/static/exports/RSCG.json @@ -2033,6 +2033,14 @@ "Source": "https://github.com/ANcpLua/ErrorOrX", "Category": "API", "AddedOn": "2026-02-02T00:00:00" + }, + { + "Name": "KnockOff", + "Link": "https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff", + "NuGet": "https://www.nuget.org/packages/KnockOff/", + "Source": "https://github.com/NeatooDotNet/KnockOff", + "Category": "Tests", + "AddedOn": "2026-02-13T00:00:00" } ] } \ No newline at end of file diff --git a/v2/rscg_examples_site/static/exports/RSCG.xlsx b/v2/rscg_examples_site/static/exports/RSCG.xlsx index a590b312a..71ca47d9d 100644 Binary files a/v2/rscg_examples_site/static/exports/RSCG.xlsx and b/v2/rscg_examples_site/static/exports/RSCG.xlsx differ diff --git a/v2/rscg_examples_site/static/sources/KnockOff.zip b/v2/rscg_examples_site/static/sources/KnockOff.zip new file mode 100644 index 000000000..ff47c25a2 Binary files /dev/null and b/v2/rscg_examples_site/static/sources/KnockOff.zip differ