From 348f9a83cb820dc96040ccad0ff726e8a7ee8203 Mon Sep 17 00:00:00 2001 From: David Naylor Date: Fri, 11 Nov 2022 22:46:08 +0000 Subject: [PATCH] Update documentation (#40) * IVia xml doc refinement * Quickstart documentation * docs --- README.md | 267 ++++------------- docs/About.md | 16 + docs/DI.md | 133 +++++++++ docs/README.md | 196 ------------- docs/Verify.md | 220 ++++++++++++++ docs/Via.md | 370 ++++++++++++++++++++++++ docs/assets/images/Redirect_Stack.svg | 2 +- src/DivertR/IVia.cs | 124 ++++---- test/DivertR.DemoApp/Program.cs | 17 +- test/DivertR.WebAppTests/WebAppTests.cs | 9 +- 10 files changed, 887 insertions(+), 467 deletions(-) create mode 100644 docs/About.md create mode 100644 docs/DI.md delete mode 100644 docs/README.md create mode 100644 docs/Verify.md create mode 100644 docs/Via.md diff --git a/README.md b/README.md index c2c5e8ba..ab10a7dd 100644 --- a/README.md +++ b/README.md @@ -1,238 +1,89 @@ # DivertR -.NET Dependency Injection Diversion - [![nuget](https://img.shields.io/nuget/v/DivertR.svg)](https://www.nuget.org/packages/DivertR) [![build](https://github.com/devodo/DivertR/actions/workflows/build.yml/badge.svg)](https://github.com/devodo/DivertR/actions/workflows/build.yml) -DivertR is similar to well known mocking frameworks like Moq or FakeItEasy but provides additional features for dynamically manipulating the dependency injection (DI) layer at runtime. -You can redirect dependency calls to test doubles, such as substitute instances, mocks or delegates, and then optionally relay them back to the original services. - -Many developers are already enjoying the benefits of in-process component/integration testing using Microsoft's [WebApplicationFactory (TestServer)](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests) -which also lets you customise the DI configuration, e.g. to substitute test doubles, but this can only be done once (per TestServer instantiation). - -DivertR was born out of the need to efficiently modify DI configurations between tests running against the same TestServer instance. -It has grown into a framework that facilitates testing of wired up systems, bringing a familiar unit/mocking testing style into the realm of component and integration testing, -by providing features to conveniently substitute dependency behaviour (including error conditions) and verify inputs and outputs from recorded call information. - -For a demonstration of usage view this [WebApp Testing Sample](https://github.com/devodo/DivertR/blob/main/test/DivertR.WebAppTests/WebAppTests.cs) or continue below -for a quickstart and code overview. - -# Quickstart +DivertR is a .NET library for creating proxy test doubles such as mocks, fakes and spies. +It is similar to mocking frameworks like the well known [Moq](https://github.com/moq/moq4) but provides, in addition, features for ***integration*** and ***component*** testing of wired-up systems. -## Installing +# Installing Install DivertR as a [NuGet package](https://www.nuget.org/packages/DivertR): - Install-Package DivertR +```sh +Install-Package DivertR +``` Or via the .NET command line interface: - dotnet add package DivertR +```sh +dotnet add package DivertR +``` -## Code Overview +# Feature Summary -![DivertR Via](./docs/assets/images/DivertR_Via.svg) +1. Test double proxy framework for mocking, faking, stubbing, spying, etc. +2. Method call interception and diversion with optional relay back to the original target. +3. Dynamic update and reset of proxies in a running application enabling changes between tests without requiring restart and initialisation overhead. +4. Simple plugging of proxy factories into the dependency injection container by decorating and wrapping existing registrations. +5. Proxies that wrap and forward to root (original) instances so tests run against the integrated system whilst modifying and spying on specific parts as needed. +6. A lightweight, fluent interface for configuring proxies to redirect calls to delegates or substitute instances. +7. Recording and verifying proxy calls. +8. Leveraging .NET ValueTuple types for specifying named and strongly typed call arguments that can be passed and reused e.g. in call verifications. -### Start with a Foo +# Example Usage -Given an `IFoo` interface and a `Foo` implementation: +DivertR can facilitate a style of testing where you start with a fully DI wired-up system and mock out specific parts per test. +For example, it can be used to write tests on a WebApp like this: ```csharp -public interface IFoo -{ - string Name { get; set; } - string Echo(string input); -} - -public class Foo : IFoo +[Fact] +public async Task GivenFooExistsInRepo_WhenGetFoo_ThenReturnsFoo_WithOk200() { - public string Name { get; set; } = "Foo"; - - public string Echo(string input) + // ARRANGE + var foo = new Foo { - return $"{Name}: {input}"; - } -} -``` - -With the following .NET `Microsoft.Extensions.DependencyInjection.IServiceCollection` registration: - -```csharp -IServiceCollection services = new ServiceCollection(); -services.AddTransient(); -services.AddSingleton(); // some other example registration -``` - -### Register - -Create a DivertR instance and *register* one or more DI service types of interest: - -```csharp -var diverter = new Diverter() - .Register() - .Register(); -``` - -The registered DivertR types are installed into the `IServiceCollection` by decorating existing DI registrations using a provided extension method: - -```csharp -services.Divert(diverter); -``` - -The `IServiceCollection` can now be used as usual to build the service provider and resolve dependency instances: - -```csharp -IServiceProvider provider = services.BuildServiceProvider(); -IFoo foo = provider.GetService(); - -Console.WriteLine(foo.Echo("Hello")); // "Foo: Hello" -``` - -### Redirect - -At this stage the behaviour of the resolved `IFoo` instances is unchanged. However, it can be modified using -a DivertR entity called a `Via` to configure a *redirect*: - -```csharp -IVia fooVia = diverter.Via(); -fooVia - .To(x => x.Echo(Is.Any)) // (1) - .Redirect(call => $"{call.Args[0]} DivertR"); // (2) - -Console.WriteLine(foo.Echo("Hello")); // "Hello DivertR" -``` - -The `Via` intercepts calls to the resolved `IFoo` instances. -By default calls are simply forwarded to the original registration, in this case instances of the `Foo` class. -However, after adding the redirect any calls that match the lambda expression (1) are redirected to the delegate (2). - -The call's arguments can be accessed from the `call.Args` property as an `object[]`. -However DivertR lets you optionally provide strongly typed named arguments using a ValueTuple type as follows: - -```csharp -IVia fooVia = diverter.Via(); -fooVia - .To(x => x.Echo(Is.Any)) // (1) - .Redirect<(string input, __)>(call => $"{call.Args.input} DivertR"); // (2) - -Console.WriteLine(foo.Echo("Hello")); // "Hello DivertR" -``` + Id = Guid.NewGuid(), + Name = "Foo123" + }; -> The `call.Args` property is replaced with an instance of the given ValueTuple type `(string intput, __)`. + _diverter + .Via() // Divert IFooRepository calls + .To(x => x.GetFooAsync(foo.Id)) // matching this method and argument + .Redirect(() => Task.FromResult(foo)); // by redirecting to this delegate -C# requires named ValueTuples to have at least two parameters. If the call only has a single parameter, as in the example above, -then a dummy second parameter must be provided using the special Diverter type `__`. - -Once a redirect is added it will be applied to all existing and future resolved `IFoo` instances. For example if a second `IFoo` instance is resolved: - -```csharp -IFoo foo2 = provider.GetService(); -foo2.Name = "Foo2"; - -Console.WriteLine(foo2.Echo("Hello")); // "Hello DivertR" -``` - -### Reset - -To *reset* resolved instances back to their original behaviour simply discard all redirects on the `Via` with the following call: - -```csharp -fooVia.Reset(); - -Console.WriteLine(foo.Echo("Hello")); // "Foo: Hello" -Console.WriteLine(foo2.Echo("Hello")); // "Foo2: Hello" -``` - -So far we have only been working with a single `Via` instance, i.e. `IVia` bound to the `IFoo` registration type. -However, testing a system would typically require using multiple `Vias` for different types. -These can all be reset at once by calling: - -```csharp -diverter.ResetAll(); -``` - -### Relay - -The `Via` also lets you *relay* back to the original or *root* registration -by providing the `Relay.Root` property that can be called from the body of the redirect: - -```csharp -fooVia - .To(x => x.Echo(Is.Any)) - .Redirect<(string input, __)>(call => - { - // run test code before - // ... - - // call root instance - IFoo root = call.Relay.Root; - var message = root.Echo(call.Args.input); - - // run test code after - // ... + // ACT + var response = await _fooClient.GetFooAsync(foo.Id); - return $"{message} Redirect"; - }); - -Console.WriteLine(foo.Echo("Hello")); // "Foo: Hello Redirect" -Console.WriteLine(foo2.Echo("Hello")); // "Foo2: Hello Redirect" -``` - -> The `Relay.Root` property is a proxy that the `Via` connects to the current intercepted call. -> Its members can only be accessed within the context of the intercepted call otherwise a `DiverterException` is thrown. - -### Retarget - -As well as redirecting to delegates you can also *retarget* to substitutes that implement the target interface (in this case `IFoo`). -This includes, for example, Mock objects: - -```csharp -IFoo root = fooVia.Relay.Root; -var mock = new Mock(); -mock - .Setup(x => x.Echo(It.IsAny())) - .Returns((string input) => $"{root.Echo(input)} Mock"); - -fooVia - .To() // No parameter defaults to match all calls - .Retarget(mock.Object); - -Console.WriteLine(foo.Echo("Hello")); // "Foo: Hello Mock" -Console.WriteLine(foo2.Echo("Hello")); // "Foo2: Hello Mock" -``` - -Note the substitute/mock can also use the `Relay.Root` proxy to call the original by conveniently accessing it as a property directly from the `Via` instance. -DivertR uses an ambient `AsyncLocal` context for this so it always points to the *root* of the current call. - -## Interfaces only - -By default DivertR can only be used on interface types. Classes are not supported as calls to non-virtual members -cannot be intercepted, causing inconsistent and confusing behaviour. + // ASSERT + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Content.Id.ShouldBe(foo.Id); + response.Content.Name.ShouldBe(foo.Name); +} -## Async support +[Fact] +public async Task GivenFooRepoException_WhenGetFoo_ThenReturns500InternalServerError() +{ + // ARRANGE + _diverter + .Via() + .To(x => x.GetFooAsync(Is.Any)) + .Redirect(() => throw new Exception()); -Task and ValueTask async calls are fully supported, e.g. if `IFoo` is extended to include an async method: + // ACT + var response = await _fooClient.GetFooAsync(Guid.NewGuid()); -```csharp -public interface IFoo -{ - Task EchoAsync(string input); + // ASSERT + response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); } +``` -public class Foo : IFoo -{ - public async Task EchoAsync(string input) - { - await Task.Yield(); - return $"{Name}: {input}"; - } -} +# Quickstart -fooVia - .To(x => x.EchoAsync(Is.Any)) - .Redirect<(string input, __)>(async (call, args) => $"{await call.Root.EchoAsync(args.input)} Async"); +For more examples and a demonstration of setting up a test harness for a WebApp see this [WebApp Testing Sample](./test/DivertR.WebAppTests/WebAppTests.cs) +or follow below for a quickstart: -Console.WriteLine(await foo.EchoAsync("Hello")); // "Foo: Hello Async" -Console.WriteLine(await foo2.EchoAsync("Hello")); // "Foo2: Hello Async" -``` +* [Vias](./docs/Via.md) for creating and configuring proxies. +* [Recording and Verifying](./docs/Verify.md) calls. +* [Dependency Injection](./docs/DI.md) integration. +* [About](./docs/About.md) DivertR. diff --git a/docs/About.md b/docs/About.md new file mode 100644 index 00000000..da9c22fd --- /dev/null +++ b/docs/About.md @@ -0,0 +1,16 @@ +# About + +DivertR is similar to well known mocking frameworks like Moq or FakeItEasy but provides additional features for dynamically manipulating the dependency injection (DI) layer at runtime. +You can redirect dependency calls to test doubles, such as substitute instances, mocks or delegates, and then optionally relay them back to the original services. + +Many developers are already enjoying the benefits of in-process component/integration testing using Microsoft's [WebApplicationFactory (TestServer)](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests) +which also lets you customise the DI configuration, e.g. to substitute test doubles, but this can only be done once (per TestServer instantiation). + +DivertR was born out of the need to efficiently modify DI configurations between tests running against the same TestServer instance. +It has grown into a framework that facilitates testing of wired up systems, bringing a familiar unit/mocking testing style into the realm of component and integration testing, +by providing features to conveniently substitute dependency behaviour (including error conditions) and verify inputs and outputs from recorded call information. + +# Interfaces Only + +DivertR uses the .NET Standard [DispatchProxy](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.dispatchproxy) to build proxies and this is limited to interface types only. +Although other proxy generators that support classes such as DynamicProxy can be used, calls to non-virtual members cannot be intercepted and this can cause inconsistent behaviour e.g. when wrapping root instances. diff --git a/docs/DI.md b/docs/DI.md new file mode 100644 index 00000000..0c9de39f --- /dev/null +++ b/docs/DI.md @@ -0,0 +1,133 @@ +# Dependency Injection + +DivertR is designed to be embedded easily and transparently into the dependency injection (DI) container to facilitate testing an integrated, wired-up system. +It does this by decorating existing DI service registrations with [Vias](./Via.md) that replace the originals. +These Vias create proxies that wrap the instances resolved from the originals as their default targets or *roots*. + +By default Via proxies transparently forward calls to their roots and therefore, in this initial state, the behaviour of the DI system is unchanged. +Then specific parts of the system can be modified as required by dynamically updating and resetting proxies between tests without requiring restart. + +# .NET ServiceCollection + +Out the box DivertR has support for the .NET `Microsoft.Extensions.DependencyInjection.IServiceCollection`. The examples below use the following `ServiceCollection` and its registrations: + +```csharp +IServiceCollection services = new ServiceCollection(); + +services.AddTransient(); +services.AddSingleton(); +services.AddSingleton(); +``` + +# Via Registration + +First instantiate an instance of the `Diverter` class and *register* one or more DI service types of interest that you would like be wrapped as Via proxies: + +```csharp +var diverter = new Diverter() + .Register() + .Register(); +``` + +Then call `Divert`, a provided `IServiceCollection` extension method, to install the registered types as `Via` decorators: + +```csharp +services.Divert(diverter); +``` + +The `IServiceCollection` can now be used as usual to build the service provider and resolve dependency instances: + +```csharp +IServiceProvider provider = services.BuildServiceProvider(); + +var foo = provider.GetService(); +Console.WriteLine(foo.Name); // "Foo" + +// The behaviour of the resolved foo is the same as its root e.g.: +IFoo demo = new Foo(); +Console.WriteLine(demo.Name); // "Foo"; +``` + +# Via Configuration + +The resolved `IFoo` instance above is a Via proxy generated by the underlying `IVia` decorator that uses the original DI registration to initialise the proxy root. +In its initial state the `IFoo` proxy forwards all calls directly to its root. However, this behaviour can be modified by obtaining the underlying `Via` +from the `Diverter` instance and adding a *redirect*: + +```csharp +// Obtain the underlying Via from the diverter instance +IVia fooVia = diverter.Via(); + +fooVia + .To(x => x.Name) + .Redirect(call => $"{call.Root.Name} diverted"); + +var foo = provider.GetService(); +Console.WriteLine(foo.Name); // "Foo diverted" +``` + +Any redirects added to the `Via` are applied to all its existing proxies and any resolved afterwards: + +```csharp +var foo2 = provider.GetService(); +foo2.Name = "Foo2"; +Console.WriteLine(foo2.Name); // "Foo2 diverted" +``` + +# Reset + +All Vias registered in the `Diverter` instance and resolved proxies can be *reset* to their initial state with a single call: + +```csharp +diverter.ResetAll(); + +Console.WriteLine(foo.Name); // "Foo" +Console.WriteLine(foo2.Name); // "Foo2" +``` + +# RedirectVia + +Sometimes a test needs to manipulate instances that are not directly created by the DI container. +E.g. if we assume the `IBarFactory` service registration given above is a factory that creates `IBar` instances. +These instances can be wrapped and managed as Via proxies by calling `RedirectVia` as follows: + +```csharp + +// Wrap created IBar instances as Via proxies and get a reference their Via +IVia barVia = diverter + .Via() + .To(x => x.Create(Is.Any)) + .RedirectVia(); + +var barFactory = provider.GetService(); +IBar bar = barFactory.Create("MrBar"); // The Create call now returns IBar proxies +Console.WriteLine(bar.Name); // "MrBar" + +// Add a redirect to alter behaviour +barVia + .To(x => x.Name) + .Redirect(call => call.Root.Name + " diverted"); + +Console.WriteLine(bar.Name); // "MrBar diverted" + +// ResetAll also resets RedirectVias +diverter.ResetAll(); +Console.WriteLine(bar.Name); // "MrBar" +``` + +# Proxy Lifetime + +DivertR aims to leave the original system behaviour unchanged and therefore +when existing DI registrations are replaced by Via decorators the lifetime of the registration is preserved. + +For multiple instance registrations such as transients, a separate proxy instance is created for each but all from the same Via instance. +In other words all proxies resolved from a Via decorated registration are managed from this single Via. + +# Dispose + +If a DI created root instance implements the `IDisposable` interface then the DI container manages its disposal, as usual, according to its registration lifetime. + +If a DI Via proxy is an `IDisposable` then **only** the proxy instance is disposed by the DI container and not the root. +In this case the responsibilty is left to the proxy for forwarding the dispose call to its root (and it does this by default). + +The above also applies to `IAsyncDisposable`. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 26ef4340..00000000 --- a/docs/README.md +++ /dev/null @@ -1,196 +0,0 @@ -# Developer guide - -DivertR is a testing framework that can be used to dynamically manipulate your dependency injection services at runtime. - -It works by replacing existing DI registrations with decorators that instead resolve proxy instances that relay to the originals. -These proxies are then injected into your app by the by DI service provider as usual but now any calls to dependencies will go via the proxies. -The proxies intercept calls but their default behaviour is to simply forward to the their wrapped instances (the original resolved instances). -Therefore, by default, the behaviour of the app is unchanged. However DivertR lets you configure proxies so they redirect calls to test doubles such as -substitutes, mocks or delegates. DivertR also provides a way for the test doubles to conveniently relay calls back to the original instances. - -A rich, fluent interface is provided for configuring proxies and this can be done at any time allowing you to modify the -behaviour of the app while the process is running. The process does not need to be restarted between configuration changes and -this can greatly speeds up the execution time of tests. For example the WebApplicationFactory (TestServer) test harness lets you run your -ASP.NET app in process and it allows you to modify the DI configuration but each configuration customisation requires a process restart -which can be slow. - -This facilitates an integrated approach to testing by making it easy to hotswap test code in and out at the DI layer. - -DivertR allows you to modify your DI services at runtime by redirecting calls to test doubles, such as substitute instances, mocks or delegates, and then optionally relaying back to the original services. -Update and reset redirect configurations, on the fly, while the process is running. - -![DivertR Via](./assets/images/DivertR_Via.svg) - -The proxies are created and configured by a DivertR entity called a `Via`. The default proxy behaviour -is to simply relay all intercepted calls directly through to the original instances. This means that -by default DivertR does not change the behaviour of the system. - -To modify the proxy behaviour the `Via` is used to add a `Redirect`. The proxy intercepts calls and forwards them to -the `Redirect` for handling. A `Redirect` is a test double that can be a delegate or any substitute that implements -the proxy interface (including Mock objects). - - -Calls are intercepted by the proxy and then passed on to the -`Redirect`s. If there a multiple `Redirect`s configured they form a stack. -it on to a test double that is any substitute that implements the original interface (including Mock objects) or a delegate. - -The `Via` provides a `Relay` interface that can be used by test doubles to conveniently call back into the original instances. -Finally the `Via` can be `Reset` any time removing all `Redirect` configurations and reverting back to the -default behaviour of relaying directly to the original. - -# Usage - -## Example ServiceCollection - -The instructions in this guide will use the following .NET `Microsoft.Extensions.DependencyInjection.IServiceCollection` registration: - -```csharp -IServiceCollection services = new ServiceCollection(); -services.AddTransient(); -services.AddTransient(); -``` - -The registered interfaces and their implementation are: - -```csharp -public interface IFoo -{ - string Name { get; set; } - string Echo(string input); - Task EchoAsync(string input); - T EchoGeneric(T input); -} - -public interface IBar -{ - IFoo Foo { get; } -} - -public class Foo : IFoo -{ - public string Name { get; set; } = "original"; - - public string Echo(string input) - { - return $"{Name}: {input}"; - } - - public async Task EchoAsync(string input) - { - await Task.Yield(); - return $"{Name}: {input}"; - } - - public T EchoGeneric(T input) - { - return input; - } -} - -public class Bar : IBar -{ - public Bar(IFoo foo) - { - Foo = foo; - } - - public IFoo Foo { get; } -} -``` - -## DivertR - -### Register - -First instantiate a `Diverter` instance and register types you want to manipulate as follows: - -```csharp -IDiverter diverter = new Diverter(); -diverter.Register(); -diverter.Register(); -``` - -Or more conveniently: - -```csharp -var diverter = new Diverter() - .Register() - .Register(); -``` - -Or: - -```csharp -var diverter = new Diverter().Register(new[] - { - typeof(IFoo), typeof(IBar) - }); -``` - -### Install - -The registered DivertR types are installed into the `IServiceCollection` using a provided extension method: - -```csharp -services.Divert(diverter); -``` - -This decorates the existing DI registrations, turning them into `Via` proxy factories. - - - -The behaviour Redirect added. -The `Via` also allows diverted calls to conveniently call back into the original instances. -DivertR makes available *relay* proxies that reference the . -Calls to the original instances can be redirected and relayed back to the original instances - - -DivertR is a proxy interception framework built on top of DispatchProxy that -Similar to mocking frameworks like Moq lets you manipulate Dependency Injection -services by decorating registrations with configurable proxies. These proxies -share a lot of similarities - -In process integration and component testing - -Note the Mock also calls the `Relay.Next` property. However, it does not relay to the original registration as before. -Instead it goes to the delegate redirect that was previously added. -This is because adding a new redirect does not replace the existing ones. Instead they are pushed onto a stack -that the `Relay.Next` property traverses... - -### Redirect chain - -![Redirect Stack](./assets/images/Redirect_Stack.svg) - -When redirects are added they are pushed onto a stack. When the `Via` intercepts a call -it traverses down the stack, starting from the last redirect added. The call is passed to the first eligible redirect (e.g. its lambda expression constraint matches). -The redirect is then responsible for executing the call and can optionally continue back down the stack by calling the `Relay.Next` property. This will again traverse the stack -until the next matching redirect is found. When there are no more redirects the original instance is called. -> In summary, the redirects are stacked forming a chain of responsibility pipeline that is -> traversed with the `Relay.Next` property. - -### Original relay - -The `Via` also provides the `Relay.Original` property that relays directly to the original instance, -skipping over any remaining redirects. - -```csharp -IFoo original = fooVia.Relay.Original; -fooVia - .To(x => x.Echo(Is.Any)) - .Redirect((string input) => $"{original.Echo(input)} - Skipped"); - -Console.WriteLine(foo.Echo("Hello")); // "Foo: Hello - Skipped" -Console.WriteLine(foo2.Echo("Hello")); // "Foo2: Hello - Skipped" -``` - -> Similar to the `Relay.Next` property, `Relay.Original` is a proxy interface that relays to the original instance -> but its members can only be accessed within the context of a `Via` intercepted call. - -### Open generic registrations -IServiceCollection supports open generic registrations for example: -services.AddTransient(typeof(IBar<>), typeof(Bar<>); - -It is not possible to DivertR let - - - diff --git a/docs/Verify.md b/docs/Verify.md new file mode 100644 index 00000000..229facea --- /dev/null +++ b/docs/Verify.md @@ -0,0 +1,220 @@ +# Recording and Verifying Calls + +DivertR can record the details of calls to its proxies and this can be used for test spying and verification. + +## Record + +The Via fluent interface is used to start a recording of proxy calls that match a `To` expression: + +```csharp +var fooVia = new Via(); +var fooProxy = fooVia.Proxy(new Foo()); + +var echoCalls = fooVia + .To(x => x.Echo(Is.Any)) // Call match expression + .Record(); // Returns an ICallStream collection of all recorded calls + +var result = fooProxy.Echo("record test"); +Console.WriteLine(result); // "record test" + +// ICallStream is an `IReadOnlyCollection` +Console.WriteLine(echoCalls.Count); // 1 + +for(var call in echoCalls) +{ + Console.WriteLine(call.Args[0]); // "record test" + Console.WriteLine(call.Returned.Value); // "record test" +} +``` + +The `Record` method returns an `ICallStream` that recorded called are appended to. This variable is an `IReadOnlyCollection` and `IEnumerable` that can be enumerated or queried e.g. with standard Linq expressions. + +## Verify + +The `ICallStream` interface provides `Verify` helper methods to facilitate iteration and verification over the recorded calls collection. + +```csharp +var fooVia = new Via(); +var fooProxy = fooVia.Proxy(new Foo()); + +var nameCalls = fooVia + .To(x => x.Echo(Is.Any)) + .Record(); + +var result = fooProxy.Echo("record"); + +var verifySnapshot = nameCalls.Verify(call => +{ + call.Args[0].ShouldBe("record"); + call.Returned.Value.ShouldBe("record"); +}); + +verifySnapshot.Count.ShouldBe(1); // The verify snapshot records calls at a point in time and is immutable +``` + +### Verify snapshot + +Recorded calls are appended to the `ICallStream` whenever a matching proxy call is made. This means the `ICallStream` may hold different record data at different points in time. +Therefore the `Verify` method iterates over and returns an immutable snapshot of the collection of recorded calls at the point of time it is called. +This allows performing multiple, consistent operations on a stable set of data. E.g. in the example above iterating over the collection and then verifying the call count. + +```csharp +var echoCalls = fooVia + .To(x => x.Echo(Is.Any)) + .Record(); + +fooProxy.Echo("one"); +Console.WriteLine(echoCalls.Count); // 1 + +// Create verify snapshot +var verifyCalls = echoCalls.Verify(); + +// Call recorded method again +fooProxy.Echo("two"); + +// Record call stream has both calls +Console.WriteLine(echoCalls.Count); // 2 + +// Snapshot is immutable +Console.WriteLine(verifyCalls.Count); // 1 +Console.WriteLine(verifyCalls[0].Args[0]); // one +``` + +## Record chaining + +The Via fluent interface allows chaining the `Record` method after a `Redirect` call: + +```csharp +var nameCalls = fooVia + .To(x => x.Echo(Is.Any)) + .Redirect(call => call.CallNext() + " redirected") + .Record(); + +var result = fooProxy.Echo("record"); + +nameCalls.Verify(call => +{ + call.Args[0].ShouldBe("record"); + call.Returned.Value.ShouldBe("record redirected"); +}).Count.ShouldBe(1); +``` + +## Verify named arguments + +The `Verify` methods allows specifying call argument types and names using the same Redirect [`ValueTuple` syntax](#named-arguments): + +```csharp +var nameCalls = fooVia + .To(x => x.Echo(Is.Any)) + .Record(); + +var result = fooProxy.Echo("record example"); + +nameCalls.Verify<(string input, __)>(call => +{ + call.Args.input.ShouldBe("record example"); + call.Returned.Value.ShouldBe("record example echo"); +}).Count.ShouldBe(1); +``` + +The argument `ValueTuple` can also be defined on the `Record` method and gets passed through to `Verify` calls: + +```csharp +var nameCalls = fooVia + .To(x => x.Echo(Is.Any)) + .Record<(string input, __)>(); + +var result = fooProxy.Echo("record example"); + +nameCalls.Verify(call => +{ + call.Args.input.ShouldBe("record example"); + call.Returned.Value.ShouldBe("record example echo"); +}).Count.ShouldBe(1); +``` + +Finally if the argument `ValueTuple` is defined on a chained `Redirect` the strongly typed argument information is passed through to the `Record` and can be used in the `Verify` calls: + +```csharp +var nameCalls = fooVia + .To(x => x.Echo(Is.Any)) + .Redirect<(string input, __)>(call => $"{call.Args.input} redirected") + .Record(); + +var result = fooProxy.Echo("record example"); + +nameCalls.Verify(call => +{ + call.Args.input.ShouldBe("record example"); + call.Returned.Value.ShouldBe("record example redirected"); +}).Count.ShouldBe(1); +``` + +## Recording exceptions + +```csharp +var nameCalls = fooVia + .To(x => x.Echo(Is.Any)) + .Redirect<(string input, __)>(() => throw new Exception()) + .Record(); + +Exception caughtException = null; +try +{ + fooProxy.Echo("record example"); +} +catch (Exception ex) +{ + caughtException = ex; +} + +nameCalls.Verify(call => +{ + call.Args.input.ShouldBe("record example"); + call.Returned.Exception.ShouldBe(caughtException) + call.Returned.Value.ShouldBeNull(); +}).Count.ShouldBe(1); +``` + +## Via Record + +```csharp +var fooVia = new Via(); +var fooProxy = fooVia.Proxy(new Foo()); + +// Record all Via proxy calls +var fooCalls = fooVia.Record(); + +fooProxy.Echo("record"); + +fooCalls + .To(x => x.Echo(Is.Any)) // Use the 'To' expression to filter Via recorded calls + .Verify<(string input, __)>(call => + { + call.Args.input.ShouldBe("record"); + call.Returned.Value.ShouldBe("record"); + }).Count.ShouldBe(1); +``` + +## Record ordering + +```csharp +var fooVia = new Via(); +var fooProxy = fooVia.Proxy(new Foo()); + +var fooCalls = fooVia.Record(opt => opt.OrderFirst()); + +fooVia + .To(x => x.Echo(Is.Any)) + .Redirect<(string input, __)>(call => call.CallNext() + " redirected") + +fooProxy.Echo("record"); + +fooCalls + .To(x => x.Echo(Is.Any)) + .Verify<(string input, __)>(call => + { + call.Args.input.ShouldBe("record"); + call.Returned.Value.ShouldBe("record redirected"); + }).Count.ShouldBe(1); +``` diff --git a/docs/Via.md b/docs/Via.md new file mode 100644 index 00000000..b11c0871 --- /dev/null +++ b/docs/Via.md @@ -0,0 +1,370 @@ +# Via + +Vias are the main DivertR entities used to create and configure proxies. +`Via` instances are instantiated from the generic `Via` class: + +```csharp +IVia fooVia = new Via(); +``` + +# Proxy + +A `Via` creates proxy objects of its generic `TTarget` type. +E.g. an `IVia` like the one instantiated above creates `IFoo` proxies: + +```csharp +var fooVia = new Via(); + +IFoo fooProxy = fooVia.Proxy(); // Create a proxy +IFoo fooTwo = fooVia.Proxy(); // Create another proxy +``` + +> A single Via can create any number of proxies. + +## Proxy Root + +When a proxy object is created it can be given a *root* instance of its target type. +The default behaviour of a proxy is to relay all calls to its root: + +```csharp +IFoo fooRoot = new Foo("MrFoo"); +Console.WriteLine(fooRoot.Name); // "MrFoo" + +var fooVia = new Via(); +IFoo fooProxy = fooVia.Proxy(fooRoot); +Console.WriteLine(fooProxy.Name); // "MrFoo" +``` + +When a proxy is in its initial state it is transparent, i.e. it behaves identically to its root instance. Therefore if a system has its instances replaced with transparent proxies its original behaviour is left unchanged. + +## Dummy Root + +If no root instance is provided then the proxy is created with a *dummy* root that returns default values: + +```csharp +var fooVia = new Via(); + +var fooMock = fooVia.Proxy(); // Proxy created with dummy root +Console.WriteLine(fooMock.Name); // null +``` + +In general dummy roots return the .NET `default` of the call's return type, e.g. `null` for reference types and `0` for `int`. +There are some special cases such as `Task` types are returned as `null` valued completed Tasks. +> Proxies with dummy roots can be used as mock objects. + +# Redirect + +`Redirect` instances are added to a `Via` to control the way its proxies behave. +Proxy calls are diverted and passed to the redirect for handling. +A fluent interface is provided on the Via for building and adding redirects to itself: + +```csharp +var foo = new Foo("MrFoo"); +Console.WriteLine(foo.Name); // "MrFoo" + +var fooVia = new Via(); +var fooProxy = fooVia.Proxy(foo); +Console.WriteLine(fooProxy.Name); // "MrFoo" + +// Add a redirect to the Via +fooVia + .To(x => x.Name) // 1. Match expression + .Redirect(() => "Hello Redirect"); // 2. Redirect delegate + +Console.WriteLine(fooProxy.Name); // "Hello Redirect" +``` + +The redirect intercepts any proxy calls matching the `To` expression 1. and diverts them to the `Redirect` delegate 2. + +Redirects can be added to a Via at any time and apply immediately to all its existing proxies as well as any created afterwards. + +## Parameter matching + +If the redirect `To` expression specifies a method with parameters, these are matched to call arguments as follows: + +```csharp +// Match calls to the Echo method with any argument value +fooVia + .To(x => x.Echo(Is.Any)) + .Redirect(() => "any"); + +// Match calls with arguments that satisfy a Match expression +fooVia + .To(x => x.Echo(Is.Match(a => a == "two"))) + .Redirect(() => "match"); + +// Match calls with arguments equal to a specified value +fooVia + .To(x => x.Echo("three")) + .Redirect(() => "equal"); + +Console.WriteLine(fooProxy.Echo("one")); // "any" +Console.WriteLine(fooProxy.Echo("two")); // "match" +Console.WriteLine(fooProxy.Echo("three")); // "equal" +``` + +## Call arguments + +Proxy call arguments can be passed to the redirect delegate as follows: + +```csharp +fooVia + .To(x => x.Echo(Is.Any)) + .Redirect(call => $"{call.Args[0]} redirected"); + +Console.WriteLine(fooProxy.Echo("me")); // "me redirected" +``` +> The `Args` property is an `IReadOnlyList` collection. + +## Named arguments + +Strongly typed and named arguments can be specified by defining a `ValueTuple` generic type on the `Redirect` method as follows: + +```csharp +fooVia + .To(x => x.Echo(Is.Any)) + .Redirect<(string input, __)>(call => $"{call.Args.input} redirected"); + +Console.WriteLine(fooProxy.Echo("me")); // "me redirected" +``` + +Call arguments are mapped in parameter order onto the `ValueTuple` items and it replaces the `Args` property from the previous example. + +The special Diverter type `__` (double underscore) is used to specify a discard mapping that is ignored so that only named types of parameters that will be used need to be defined. + +C# requires named ValueTuples to have at least two parameters. If the call only has a single parameter, as in the example above, +then the discard type `__` must be used to provide a second dummy parameter. + +## Relay root + +The redirect delegate can *relay* calls back to the proxy root by calling the `Relay.Root` property: + +```csharp +fooVia + .To(x => x.Name) + .Redirect(call => + { + IFoo root = call.Relay.Root; + return $"{root.Name} relayed"; + }); + +Console.WriteLine(fooRoot.Name); // "MrFoo" +Console.WriteLine(fooProxy.Name); // "MrFoo relayed" +``` + +> The `Relay.Root` property is a proxy that relays calls to the root instance. + +## Relay next + +![Redirect Stack](./assets/images/Redirect_Stack.svg) + +Any number of redirects can be added to a Via. When redirects are added they are pushed onto a stack (with the last added at the top). +Proxy calls are traversed through the stack from top to bottom. If a call matches the `To` constraint it is passed to the redirect delegate for handling. +If no redirects match, the call falls through the stack to the root instance. + +Redirect delegates can relay the call directly to the root as in the previous example +but they can also continue the call down the redirect stack by calling the `Relay.Next` property as follows: + +```csharp +fooVia + .To(x => x.Name) + .Redirect(call => $"{call.Relay.Next.Name} 1") + .Redirect(call => $"{call.Relay.Next.Name} 2") // Redirect calls can be chained + .Redirect(call => $"{call.Next.Name} 3"); // The Root and Next properties can be accessed directly from the call argument + +Console.WriteLine(fooRoot.Name); // "MrFoo" +Console.WriteLine(fooProxy.Name); // "MrFoo 1 2 3" +``` + +> The `Relay.Next` property is a proxy that relays calls to the next redirect that matches. +> If no redirects match it will relay to the root. +> The Root and Next properties can also be accessed directly from the call argument for convenience. + +## Call forwarding + +A redirect can call `CallRoot()` to forward the call to the target method of the root instance: + +```csharp +fooVia + .To(x => x.Name) + .Redirect(call => call.CallRoot() + " 1"); + +Console.WriteLine(fooRoot.Name); // "MrFoo" +Console.WriteLine(fooProxy.Name); // "MrFoo 1" +``` + +Or the call can be forwarded down the redirect stack using `CallNext()`: + +```csharp +fooVia + .To(x => x.Name) + .Redirect(call => call.CallNext() + " 1") + .Redirect(call => call.CallNext() + " 2"); + +Console.WriteLine(fooRoot.Name); // "MrFoo" +Console.WriteLine(fooProxy.Name); // "MrFoo 1 2" +``` + +If the target method has parameters then `CallRoot()` and `CallNext()` forward the arguments from the original call. Custom arguments can be forwarded by passing an `object[]` to `CallRoot()` or `CallNext()`: + +```csharp +fooVia + .To(x => x.Echo(Is.Any)) + .Redirect(call => call.CallNext(new[] { "you" })); + +Console.WriteLine(fooProxy.Echo("me")); // "you" +``` + +## Void methods + +For redirect methods that return `void`, the same `Via` fluent interface syntax is used, only the delegate provided is an `Action` rather than a `Func`: + +```csharp +fooVia + .To(x => x.SetAge(Is.Any)) + .Redirect<(int age, __)>(call => + { + call.CallNext() + 10; + }); +``` + +## Property Getters and Setters + +Redirects for property getters, [demonstrated earlier](#redirect), are added using the same `To` syntax as for standard methods. However, to indicate a redirect is for a property setter, the `ToSet` method is used instead: + +```csharp +fooVia + .ToSet(x => x.Name) + .Redirect<(string name, __)>(call => + { + call.Next.Name = call.Args.name + " changed"; + }); +``` + +By default the redirect above will match any setter value input but the `ToSet` method accepts a second parameter as a value match expression using the usual [parameter matching](#parameter-matching) syntax: + +```csharp +fooVia + .ToSet(x => x.Name, () => Is.Match(p => p.StartsWith("M"))) + .Redirect<(string name, __)>(call => + { + call.Next.Name = name + " changed"; + }); + +fooProxy.Name = "Me"; +Console.WriteLine(fooProxy.Name); // "Me changed" +``` + +## Async methods + +Async is fully supported by DivertR and redirect delegates can be added to `Task` or `ValueTask` methods using the standard C# `async` syntax: + +```csharp +fooVia + .To(x => x.SaveAsync(Is.Any, Is.Any)) + .Redirect(async call => + { + var result = await call.CallNext(); + + return result; + }); +``` + +## Throwing exceptions + +Redirect delegates can throw exceptions using standard C# syntax and any exceptions thrown will bubble up to callers as usual: + +```csharp +fooVia + .To(x => x.Echo("exception")) + .Redirect(() => throw new MyException()) + +fooProxy.Echo("exception"); // throws MyException +``` + +## Generic methods + +```csharp +fooVia + .To(x => x.Echo(Is.Any)) + .Redirect(call => call.CallNext() * 2); + +``` + +# Reset + +A Via can be *reset* which removes all its redirects, reverting its proxies to their original behaviour: + +```csharp +fooVia.To(x => x.Name).Redirect("diverted"); +Console.WriteLine(fooProxy.Name); // "diverted" + +fooVia.Reset(); + +Console.WriteLine(fooProxy.Name); // "MrFoo" +``` + +The reset method on the Via can be called at any time and is applied immediately to all its proxies. +By adding redirects and resetting, proxy behaviour can be modified at runtime allowing a running process to be altered between tests, e.g. to avoid restart and initialisation overhead. + +# Retarget + +A Via can be configured to *retarget* its proxy calls to a substitute instance of its target type: + +```csharp +var fooVia = new Via(); +var fooProxy = fooVia.Proxy(new Foo("MrFoo")); +Console.WriteLine(fooProxy.Name); // "MrFoo" + +var fooTwo = new Foo("two"); +Console.WriteLine(fooTwo.Name); // "two" + +fooVia.Retarget(fooTwo); + +Console.WriteLine(fooProxy.Name); // "two" +``` + +The retarget substitute can be any instance of the Via's target type including e.g. Mock objects: + +```csharp +var mock = new Mock(); +mock + .Setup(x => x.Echo(It.IsAny())) + .Returns((string input) => $"{input} mock"); + +fooVia.Retarget(mock.Object); + +Console.WriteLine(fooProxy.Echo("hello")); // "hello mock" +``` + +When a `Retarget` is added it is also pushed onto the Via redirect stack. +Retarget substitutes are also able to relay calls to the proxy root or next redirect from the Via's `Relay` property: + +```csharp +IFoo next = fooVia.Relay.Next; +IFoo root = fooVia.Relay.Root; +mock + .Setup(x => x.Name) + .Returns(() => $"{root.Name} {next.Name} mock"); + +Console.WriteLine(fooProxy.Name); // "MrFoo two mock" +``` + +# Strict mode + +Enable *strict mode* on a Via to ensure only methods with registered redirects are allowed to be called: + +```csharp +fooVia.Strict(); // enables strict mode + +fooVia + .To(x => x.Echo("ok")) + .Redirect(call => call.Args[0]); + +fooProxy.Echo("me"); // throws StrictNotSatisfiedException +fooProxy.Echo("ok"); // "ok" +``` + +When strict mode is enabled a `StrictNotSatisfiedException` is thrown if a call is made to a proxy and does not match any redirects. + +Strict mode is disabled when a Via is created or [reset](#reset). diff --git a/docs/assets/images/Redirect_Stack.svg b/docs/assets/images/Redirect_Stack.svg index 99e97e5b..389c587f 100644 --- a/docs/assets/images/Redirect_Stack.svg +++ b/docs/assets/images/Redirect_Stack.svg @@ -1,3 +1,3 @@ -
Redirect Stack
Redirect Stack
Via Proxy
Via Proxy
Original
Original
Call
Call
Redirect 1
Redirect 1
Redirect 2
Redirect 2
Redirect 3
Redirect 3
Viewer does not support full SVG 1.1
\ No newline at end of file +
Redirect Stack
Redirect Stack
Via Proxy
Via Proxy
Root
Root
Call
Call
Redirect 1
Redirect 1
Redirect 2
Redirect 2
Redirect 3
Redirect 3
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/src/DivertR/IVia.cs b/src/DivertR/IVia.cs index 943350fd..0f915ad2 100644 --- a/src/DivertR/IVia.cs +++ b/src/DivertR/IVia.cs @@ -6,77 +6,89 @@ namespace DivertR { /// - /// A Via is the DivertR unit associated with a type. The Via is used to create proxies of its type and to configure the proxy behaviour. + /// Vias are used to create DivertR proxies and configure their behaviour. + /// A Via instance has a single, fixed target type and it creates proxies of this type. + /// + /// Proxy behaviour is configured by inserting one or more s to the Via. The configured redirects are applied to all proxies created by the Via. + /// Redirects can be added or removed from the Via at any time allowing the proxy behaviour to be changed dynamically at runtime. + /// + /// A proxy is created with a reference to root instance of its type and by default it forwards all its call to this root, i.e. when no redirects are configured on the Via. + /// If a root instance is not provided the proxy will be created with a dummy root that provides default return values on its members. + /// Optionally a proxy can also be created with a null root but in this case the proxy behaviour must be defined to handle any call received else + /// a will be thrown. /// public interface IVia { /// - /// The Via identifier consisting of its type and optional name label. + /// The Via identifier that is a composite of the target type and an optional group name. /// ViaId ViaId { get; } /// - /// The of this Via + /// The of this Via. The ViaSet contains a collection of Vias each with a unique . /// IViaSet ViaSet { get; } /// - /// Reference to the Via chain of responsibility call pipeline. + /// The Via that is used to access the chain of responsibility redirect pipeline within proxy calls. /// IRelay Relay { get; } /// - /// The containing the current proxy redirect configuration. + /// The Via's for storing and managing the redirect configuration that determines proxy behaviour. /// IRedirectRepository RedirectRepository { get; } /// - /// Create a Via proxy object without needing to specify the compile time Via type. + /// Creates a Via proxy object of the Via's target type. /// - /// Optional root instance to proxy calls to. - /// Thrown if is not the Via type. + /// Optional root instance the proxy will relay calls to. + /// Thrown if is not the Via target type. /// The proxy instance. object Proxy(object? root); /// - /// Create a Via proxy instance with no provided root instance. + /// Creates a Via proxy object of the Via's target type with no provided root instance. /// /// Specifies if the proxy should be created with a dummy root or not. /// The proxy instance. object Proxy(bool withDummyRoot); /// - /// Create a Via proxy instance with no provided root instance. By default the proxy will be created with a dummy root but this can be configured in the + /// Creates a Via proxy object of the Via's target type with no provided root instance. + /// The proxy is created with a dummy root or not as configured by the Via's . /// /// The proxy instance. object Proxy(); /// - /// Insert a redirect into this Via. + /// Inserts a redirect into this Via. /// /// The redirect instance. /// Optional redirect options builder action. - /// The current instance. + /// This Via instance. IVia Redirect(IRedirect redirect, Action? optionsAction = null); /// - /// Reset the Via. + /// Reset the Via removing all configured redirects and disabling strict mode. /// - /// The current instance. + /// This Via instance. IVia Reset(); /// - /// Set strict mode. If no argument, strict mode is enabled by default. + /// Set strict mode on the Via. + /// If strict is enabled on the Via and a call to its proxies does not hit a configured + /// then a is thrown. /// /// Optional bool to specify enable/disable of strict mode. - /// The current instance. + /// This Via instance. IVia Strict(bool? isStrict = true); } /// - /// Strongly typed Via class used to create DivertR proxies of its type and to configure the proxy behaviour. + /// Via interface with generic type defining the proxy target type. /// - /// The Via type. + /// The proxy target type. public interface IVia : IVia where TTarget : class? { /// @@ -85,24 +97,24 @@ public interface IVia : IVia where TTarget : class? new IRelay Relay { get; } /// - /// Create a Via proxy instance. + /// Creates a Via proxy instance of the Via's target type. /// - /// Optional root instance to proxy calls to. + /// Optional root instance the proxy will relay calls to. /// The proxy instance. [return: NotNull] TTarget Proxy(TTarget? root); /// - /// Create a Via proxy object without needing to specify the compile time Via type. + /// Creates a Via proxy instance of the Via's target type. /// - /// Optional root instance to proxy calls to. - /// Thrown if is not the Via type. + /// Optional root instance the proxy will relay calls to. + /// Thrown if is not the Via target type. /// The proxy instance. [return: NotNull] new TTarget Proxy(object? root); /// - /// Create a Via proxy instance with no provided root instance. + /// Creates a Via proxy instance of the Via's target type with no provided root instance. /// /// Specifies if the proxy should be created with a dummy root or not. /// The proxy instance. @@ -110,7 +122,8 @@ public interface IVia : IVia where TTarget : class? new TTarget Proxy(bool withDummyRoot); /// - /// Create a Via proxy instance with no provided root instance. By default the proxy will be created with a dummy root but this can be configured in the + /// Creates a Via proxy instance of the Via's target type with no provided root instance. + /// The proxy is created with a dummy root or not as configured by the Via's . /// /// The proxy instance. [return: NotNull] @@ -121,64 +134,65 @@ public interface IVia : IVia where TTarget : class? /// /// The redirect instance. /// Optional redirect options builder action. - /// The current instance. + /// This Via instance. new IVia Redirect(IRedirect redirect, Action? optionsAction = null); + + /// + /// Reset the Via removing all configured redirects and disabling strict mode. + /// + /// This Via instance. + new IVia Reset(); /// - /// Create and insert a redirect (with no ) to the given - /// into the Via . + /// Set strict mode on the Via. + /// If strict is enabled on the Via and a call to its proxies does not hit a configured + /// then a is thrown. /// - /// The target instance to redirect call to. + /// Optional bool to specify enable/disable of strict mode. + /// This Via instance. + new IVia Strict(bool? isStrict = true); + + /// + /// Inserts a retarget redirect with no call constraints (therefore all calls will be redirected). + /// + /// The target instance to retarget calls to. /// An optional builder action for configuring redirect options. - /// The current instance. + /// This Via instance. IVia Retarget(TTarget target, Action? optionsAction = null); /// - /// Inserts a redirect that captures incoming calls from all proxies. + /// Inserts a record redirect that captures incoming calls from all proxies. /// By default record redirects are configured to not satisfy strict calls if strict mode is enabled. /// /// An optional builder action for configuring redirect options. - /// An reference for retrieving and iterating the recorded calls. + /// An instance for retrieving and iterating the recorded calls. IRecordStream Record(Action? optionsAction = null); - - /// - /// Reset the Via . - /// - /// The current instance. - new IVia Reset(); /// - /// Enable strict mode. + /// Creates a instance that is used to insert redirects to this Via. /> /// - /// Optional bool to specify enable/disable of strict mode. - /// The current instance. - new IVia Strict(bool? isStrict = true); - - /// - /// Creates a Redirect builder. /> - /// - /// Optional redirect . - /// The Redirect builder instance. - /// + /// Optional call constraint . + /// The builder instance. IViaBuilder To(ICallConstraint? callConstraint = null); /// - /// Creates a Redirect builder from an Expression with a call constraint that matches a member of returning . + /// Creates a instance that is used to insert redirects to this Via for calls matching the expression. /// /// The call constraint expression. /// The Expression return type - /// The Redirect builder instance. + /// The builder instance. IFuncViaBuilder To(Expression> constraintExpression); /// - /// Creates a Redirect builder from an Expression with a call constraint that matches a member of returning void />. + /// Creates a that is used to insert redirects to this Via for void calls matching the expression. /// /// The call constraint expression. - /// The Redirect builder instance. + /// The builder instance. IActionViaBuilder To(Expression> constraintExpression); /// - /// Creates a Redirect builder from an Expression with a call constraint that matches a property setter member of />. + /// Creates a that is used to insert redirects to this Via for setter calls to matching the property expression + /// and setter value expression. /// /// The expression for matching the property setter member. /// Optional constraint expression on the setter input argument. If null, the constraint defaults to match any value @@ -186,4 +200,4 @@ public interface IVia : IVia where TTarget : class? /// The Redirect builder instance. IActionViaBuilder ToSet(Expression> memberExpression, Expression>? constraintExpression = null); } -} \ No newline at end of file +} diff --git a/test/DivertR.DemoApp/Program.cs b/test/DivertR.DemoApp/Program.cs index 102151cf..beeda347 100644 --- a/test/DivertR.DemoApp/Program.cs +++ b/test/DivertR.DemoApp/Program.cs @@ -11,7 +11,7 @@ public interface IFoo string Name { get; set; } string Echo(string input); Task EchoAsync(string input); - T EchoGeneric(T input); + T Echo(T input); } public interface IBar @@ -21,7 +21,7 @@ public interface IBar public class Foo : IFoo { - public string Name { get; set; } = "original"; + public string Name { get; set; } = "Foo"; public string Echo(string input) { @@ -34,7 +34,7 @@ public async Task EchoAsync(string input) return $"{Name}: {input}"; } - public T EchoGeneric(T input) + public T Echo(T input) { return input; } @@ -136,6 +136,17 @@ static async Task Main() Console.WriteLine(await foo.EchoAsync("Hello")); // "Foo1: Hello - Async" Console.WriteLine(await foo2.EchoAsync("Hello")); // "Foo2: Hello - Async" + + fooVia + .To(x => x.Echo(Is.Any)) + .Redirect(call => call.CallNext() + 10); + + fooVia + .To(x => x.Echo(Is>.Any)) + .Redirect(async call => await call.CallNext() + 100); + + Console.WriteLine(foo.Echo(5)); // 15 + Console.WriteLine(await foo.Echo(Task.FromResult(50))); // 150 } } } \ No newline at end of file diff --git a/test/DivertR.WebAppTests/WebAppTests.cs b/test/DivertR.WebAppTests/WebAppTests.cs index 07f6c93b..2d1b8947 100644 --- a/test/DivertR.WebAppTests/WebAppTests.cs +++ b/test/DivertR.WebAppTests/WebAppTests.cs @@ -24,7 +24,7 @@ public WebAppTests(WebAppFixture webAppFixture, ITestOutputHelper output) } [Fact] - public async Task GivenFooExistsInRepo_WhenGetFoo_ThenReturnFooContent_WithOk200() + public async Task GivenFooExistsInRepo_WhenGetFoo_ThenReturnsFoo_WithOk200() { // ARRANGE var foo = new Foo @@ -48,7 +48,7 @@ public async Task GivenFooExistsInRepo_WhenGetFoo_ThenReturnFooContent_WithOk200 } [Fact] - public async Task GivenAnyFooExistsInRepo_WhenGetFoo_ThenReturnFooContent_WithOk200() + public async Task GivenAnyFooExistsInRepo_WhenGetFoo_ThenReturnsFoot_WithOk200() { // ARRANGE var fooId = Guid.NewGuid(); @@ -83,7 +83,8 @@ public async Task GivenFooInserted_WhenGetFoo_ThenReadsFromFooRepository() // Insert foo directly into the repository var fooRepository = _services.GetRequiredService(); (await fooRepository.TryInsertFooAsync(foo)).ShouldBeTrue(); - + + // Record all repo calls var fooRepoCalls = _diverter .Via() .Record(); @@ -94,7 +95,7 @@ public async Task GivenFooInserted_WhenGetFoo_ThenReadsFromFooRepository() // ASSERT response.Content.ShouldBeEquivalentTo(foo); - // Verify repo read call + // Verify repo get method called correctly (await fooRepoCalls .To(x => x.GetFooAsync(Is.Any)) .Verify<(Guid fooId, __)>(async call =>