Redirect
Table of contents
The Redirect
is the main DivertR interface used to create and configure proxies. Redirect
instances are instantiated from the generic Redirect<TTarget>
class:
IRedirect<IFoo> fooRedirect = new Redirect<IFoo>();
Creating Proxies
A Redirect
creates proxy objects of its generic TTarget
type. E.g. an IRedirect<IFoo>
like the one instantiated above creates IFoo
proxies:
var fooRedirect = new Redirect<IFoo>();
IFoo fooProxy = fooRedirect.Proxy(); // Create a proxy
IFoo fooTwo = fooRedirect.Proxy(); // Create another proxy
A single Redirect 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:
IFoo fooRoot = new Foo("MrFoo");
Console.WriteLine(fooRoot.Name); // "MrFoo"
var fooRedirect = new Redirect<IFoo>();
IFoo fooProxy = fooRedirect.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:
var fooRedirect = new Redirect<IFoo>();
var fooMock = fooRedirect.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.
Vias
Via
instances are added to a Redirect
to control the way its proxies behave. Proxy calls are diverted and passed to the Vias for handling. A fluent interface is provided on the Redirect for building and adding Vias to itself:
var foo = new Foo("MrFoo");
Console.WriteLine(foo.Name); // "MrFoo"
var fooRedirect = new Redirect<IFoo>();
var fooProxy = fooRedirect.Proxy(foo);
Console.WriteLine(fooProxy.Name); // "MrFoo"
// Add a Via to the Redirect
fooRedirect
.To(x => x.Name) // 1. Match expression
.Via(() => "Hello Via"); // 2. Via delegate
Console.WriteLine(fooProxy.Name); // "Hello Via"
The Via
intercepts any proxy calls matching the To
expression 1. and diverts them to the Via
delegate 2.
Vias can be added to a Redirect at any time and apply immediately to all its existing proxies as well as any created afterwards.
Reset
A Redirect can be reset which removes all its Vias, reverting its proxies to their original behaviour:
fooRedirect.To(x => x.Name).Via("diverted");
Console.WriteLine(fooProxy.Name); // "diverted"
fooRedirect.Reset();
Console.WriteLine(fooProxy.Name); // "MrFoo"
Reset can be called at any time and is applied immediately to all of the Redirect’s proxies. By adding Vias 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.
After a Redirect is reset its proxies are in their default, transparent state of forwarding all calls to their root instances. This enables a pattern of testing where proxy behaviour is modified with Vias and then the system is reset to its original state between tests.
Method Parameters
Via intercept match rules can be configured on method parameters using call argument values and these can also be passed to Via delegates.
Parameter Matching
If the Via To
expression specifies a method with parameters, these are matched to call arguments as follows:
// Match calls to the Echo method with any argument value
fooRedirect
.To(x => x.Echo(Is<string>.Any))
.Via(() => "any");
// Match calls with arguments that satisfy a Match expression
fooRedirect
.To(x => x.Echo(Is<string>.Match(a => a == "two")))
.Via(() => "match");
// Match calls with arguments equal to a specified value
fooRedirect
.To(x => x.Echo("three"))
.Via(() => "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 Via delegate as follows:
fooRedirect
.To(x => x.Echo(Is<string>.Any))
.Via(call => $"{call.Args[0]} redirected");
Console.WriteLine(fooProxy.Echo("me")); // "me redirected"
The
Args
property is anIReadOnlyList<object>
collection.
Named Arguments
Strongly typed and named arguments can be specified by defining a ValueTuple
generic type on the Via
method as follows:
fooRedirect
.To(x => x.Echo(Is<string>.Any))
.Via<(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
A special feature of Redirects is their ability to control how calls are forwarded or relayed back to proxy root instances.
Relay Root
The Via delegate can relay calls back to the proxy root by calling the Relay.Root
property:
fooRedirect
.To(x => x.Name)
.Via(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
Any number of Vias can be added to a Redirect. When Vias 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 Via delegate for handling. If no Vias match, the call falls through the stack to the root instance.
Via delegates can relay the call directly to the root as in the previous example but they can also continue the call down the Via stack by calling the Relay.Next
property as follows:
fooRedirect
.To(x => x.Name)
.Via(call => $"{call.Relay.Next.Name} 1")
.Via(call => $"{call.Relay.Next.Name} 2") // Via calls can be chained
.Via(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 Via that matches. If no Vias 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 Via can call CallRoot()
to forward the call to the target method of the root instance:
fooRedirect
.To(x => x.Name)
.Via(call => call.CallRoot() + " 1");
Console.WriteLine(fooRoot.Name); // "MrFoo"
Console.WriteLine(fooProxy.Name); // "MrFoo 1"
Or the call can be forwarded down the Via stack using CallNext()
:
fooRedirect
.To(x => x.Name)
.Via(call => call.CallNext() + " 1")
.Via(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:
fooRedirect
.To(x => x.Echo(Is<string>.Any))
.Via(call => call.CallNext() + " and you");
Console.WriteLine(fooProxy.Echo("me")); // "me and you"
Custom arguments can be forwarded by passing an object[]
to CallRoot()
or CallNext()
:
fooRedirect
.To(x => x.Echo(Is<string>.Any))
.Via(call => call.CallNext(new[] { "you" }));
Console.WriteLine(fooProxy.Echo("me")); // "you"
Additional Usages
Async Methods
Async is fully supported by DivertR and Via delegates can be added to Task
or ValueTask
methods using the standard C# async
syntax:
fooRedirect
.To(x => x.SaveAsync(Is<string>.Any, Is<CancellationToken>.Any))
.Via(async call =>
{
var result = await call.CallNext();
return result;
});
Property Getters and Setters
Vias for property getters, demonstrated earlier, are added using the same To
syntax as for standard methods. However, to indicate a Via is for a property setter, the ToSet
method is used instead:
fooRedirect
.ToSet(x => x.Name)
.Via<(string name, __)>(call =>
{
call.Next.Name = call.Args.name + " changed";
});
By default the Via 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 syntax:
fooRedirect
.ToSet(x => x.Name, () => Is<string>.Match(p => p.StartsWith("M")))
.Via<(string name, __)>(call =>
{
call.Next.Name = name + " changed";
});
fooProxy.Name = "Me";
Console.WriteLine(fooProxy.Name); // "Me changed"
Void Methods
For methods that return void
, the same Redirect
fluent interface syntax is used, only the Via
delegate provided is an Action
rather than a Func
:
fooRedirect
.To(x => x.SetAge(Is<int>.Any))
.Via<(int age, __)>(call =>
{
call.Next.SetAge(call.Args.ags + 10);
});
Generic Methods
Generic method Vias are declared using the same fluent syntax ands are matched on the specified generic type arguments.
fooRedirect
.To(x => x.Echo<int>(Is<int>.Any))
.Via(call => call.CallNext() * 2);
Throwing Exceptions
Via delegates can throw exceptions using standard C# syntax and any exceptions thrown will bubble up to callers as usual:
fooRedirect
.To(x => x.Echo("exception"))
.Via(() => throw new MyException())
fooProxy.Echo("exception"); // throws MyException
Retarget
A Redirect can be configured to retarget its proxy calls to a substitute instance of its target type:
var fooRedirect = new Redirect<IFoo>();
var fooProxy = fooRedirect.Proxy(new Foo("MrFoo"));
Console.WriteLine(fooProxy.Name); // "MrFoo"
var fooTwo = new Foo("two");
Console.WriteLine(fooTwo.Name); // "two"
fooRedirect.Retarget(fooTwo);
Console.WriteLine(fooProxy.Name); // "two"
The retarget substitute can be any instance of the Redirect’s target type including e.g. Mock objects:
var mock = new Mock<IFoo>();
mock
.Setup(x => x.Echo(It.IsAny<string>()))
.Returns((string input) => $"{input} mock");
fooRedirect.Retarget(mock.Object);
Console.WriteLine(fooProxy.Echo("hello")); // "hello mock"
When a Retarget
is added it is also pushed onto the Redirect Via stack. Retarget substitutes are also able to relay calls to the proxy root or next Via from the Redirect’s Relay
property:
IFoo next = fooRedirect.Relay.Next;
IFoo root = fooRedirect.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 Redirect to ensure only methods with matching Vias are allowed to be called:
fooRedirect.Strict(); // enables strict mode
fooRedirect
.To(x => x.Echo("ok"))
.Via(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 Vias.
Strict mode is disabled when a Redirect is created or reset.