diff --git a/docs/documentation/dependency_injection.md b/docs/documentation/dependency_injection.md index 83d6b766..5f611f10 100644 --- a/docs/documentation/dependency_injection.md +++ b/docs/documentation/dependency_injection.md @@ -111,15 +111,16 @@ After reset all resolved redirect proxies will be returned to their initial stat *Nested registrations* are used to redirect calls on types that are not directly resolved by the DI container such as instances created by factory services. E.g. if we have an `IBarFactory` factory service, resolved by the DI, that has factory methods that create `IBar` instances. -A nested `IBar` registration off a parent `IBarFactory` registration can be declared as follows: +A nested `IBar` redirect registration from the parent `IBarFactory` registration can be declared as follows: ```csharp -var diverter = new Diverter() +var diverter = new DiverterBuilder() .Register(x => x - .ThenRegister()); + .ThenRedirect()) // Proxy redirect any IBar instances returned by IBarFactory + .Create(); ``` -> Nested registrations on nested registration is supported. +> Nested registrations of nested registrations can be chained to any depth. {: .note } This is installed into the existing `IServiceCollection` as usual: diff --git a/src/DivertR/DiverterBuilder.cs b/src/DivertR/DiverterBuilder.cs index 2f7db9a8..6d2c9c79 100644 --- a/src/DivertR/DiverterBuilder.cs +++ b/src/DivertR/DiverterBuilder.cs @@ -35,13 +35,13 @@ public DiverterBuilder(IRedirectSet redirectSet) public IRedirectSet RedirectSet { get; } /// - public IDiverterBuilder Register(Action? nestedRegisterAction = null) where TTarget : class? + public IDiverterBuilder Register(Action>? nestedRegisterAction = null) where TTarget : class? { - return Register(null, nestedRegisterAction); + return Register(null, nestedRegisterAction); } /// - public IDiverterBuilder Register(string? name, Action? nestedRegisterAction = null) where TTarget : class? + public IDiverterBuilder Register(string? name, Action>? nestedRegisterAction = null) where TTarget : class? { var redirect = RedirectSet.GetOrCreate(name); diff --git a/src/DivertR/IDiverterBuilder.cs b/src/DivertR/IDiverterBuilder.cs index 5a786d80..d7496ecb 100644 --- a/src/DivertR/IDiverterBuilder.cs +++ b/src/DivertR/IDiverterBuilder.cs @@ -23,7 +23,7 @@ public interface IDiverterBuilder /// The type to register. /// This instance. /// Thrown if the type with with default has already been registered. - IDiverterBuilder Register(Action? nestedRegisterAction = null) where TTarget : class?; + IDiverterBuilder Register(Action>? nestedRegisterAction = null) where TTarget : class?; /// /// Register a type to redirect. @@ -33,7 +33,7 @@ public interface IDiverterBuilder /// The type to register. /// This instance. /// Thrown if the type with matching has already been registered. - IDiverterBuilder Register(string? name, Action? nestedRegisterAction = null) where TTarget : class?; + IDiverterBuilder Register(string? name, Action>? nestedRegisterAction = null) where TTarget : class?; /// /// Register a type to redirect. diff --git a/src/DivertR/INestedRegisterBuilder.cs b/src/DivertR/INestedRegisterBuilder.cs index e4478f08..f8b0fc60 100644 --- a/src/DivertR/INestedRegisterBuilder.cs +++ b/src/DivertR/INestedRegisterBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Linq.Expressions; namespace DivertR { @@ -6,31 +7,49 @@ namespace DivertR /// A builder interface for providing nested register actions on parent registrations. /// Nested registrations can be used to intercept and redirect inner services created outside the dependency injection container e.g. from factories. /// - public interface INestedRegisterBuilder + public interface INestedRegisterBuilder where TTarget : class? { /// - /// Add a nested registration to redirect calls from the parent registration with return type - /// by proxying instances returned from parent methods via an . - /// A corresponding is created and added to the underlying . - /// The returned instances are proxied by inserting a on the that is persistent, i.e. remains after reset. + /// Redirect calls from the parent registration with return type by proxying instances returned from parent methods via an . + /// The returned instances are proxied by inserting a persistent on the . /// + /// Optional nested register action. /// The return type of calls to redirect. - /// This instance. + /// This instance. /// Thrown if a nested has already been registered on the parent with matching type and default . - INestedRegisterBuilder ThenRegister(Action? registerAction = null) where TReturn : class?; + INestedRegisterBuilder ThenRedirect(Action>? registerAction = null) where TReturn : class?; /// - /// Add a nested registration to redirect calls from the parent registration with return type - /// by proxying instances returned from parent methods via an . - /// A corresponding is created and added to the underlying . - /// The returned instances are proxied by inserting a on the that is persistent, i.e. remains after reset. + /// Redirect calls from the parent registration with return type by proxying instances returned from parent methods via an . + /// The returned instances are proxied by inserting a persistent on the . /// /// Specify the of the returned . /// Optional nested register action. /// The return type of calls to redirect. - /// This instance. + /// This instance. /// Thrown if a nested has already been registered on the parent with matching type and . - INestedRegisterBuilder ThenRegister(string? name, Action? registerAction = null) where TReturn : class?; + INestedRegisterBuilder ThenRedirect(string? name, Action>? registerAction = null) where TReturn : class?; + + /// + /// Redirect calls matching a constraint from the parent registration by proxying instances returned from parent methods via an . + /// The returned instances are proxied by inserting a persistent on the . + /// + /// The call constraint expression. + /// Optional nested register action. + /// The return type of calls to redirect. + /// This instance. + INestedRegisterBuilder ThenRedirect(Expression> constraintExpression, Action>? registerAction = null) where TReturn : class?; + + /// + /// Redirect calls matching a constraint from the parent registration by proxying instances returned from parent methods via an . + /// The returned instances are proxied by inserting a persistent on the . + /// + /// Specify the of the returned . + /// The call constraint expression. + /// Optional nested register action. + /// The return type of calls to redirect. + /// This instance. + INestedRegisterBuilder ThenRedirect(string? name, Expression> constraintExpression, Action>? registerAction = null) where TReturn : class?; /// /// Register a decorator on call returns of type on the nested redirect. @@ -38,7 +57,7 @@ public interface INestedRegisterBuilder /// /// The decorator function. /// The return type of calls to decorate. - /// This instance. - INestedRegisterBuilder ThenDecorate(Func decorator); + /// This instance. + INestedRegisterBuilder ThenDecorate(Func decorator); } } \ No newline at end of file diff --git a/src/DivertR/Internal/NestedRegisterBuilder.cs b/src/DivertR/Internal/NestedRegisterBuilder.cs index ca504f2d..37887ef7 100644 --- a/src/DivertR/Internal/NestedRegisterBuilder.cs +++ b/src/DivertR/Internal/NestedRegisterBuilder.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Concurrent; +using System.Linq.Expressions; namespace DivertR.Internal { - internal class NestedRegisterBuilder : INestedRegisterBuilder where TTarget : class? + internal class NestedRegisterBuilder : INestedRegisterBuilder where TTarget : class? { private readonly IRedirect _redirect; private readonly ConcurrentDictionary> _registeredRedirects; @@ -14,12 +15,12 @@ public NestedRegisterBuilder(IRedirect redirect, ConcurrentDictionary(Action? registerAction = null) where TReturn : class? + public INestedRegisterBuilder ThenRedirect(Action>? registerAction = null) where TReturn : class? { - return ThenRegister(null, registerAction); + return ThenRedirect((string?) null, registerAction); } - public INestedRegisterBuilder ThenRegister(string? name, Action? registerAction = null) where TReturn : class? + public INestedRegisterBuilder ThenRedirect(string? name, Action>? registerAction = null) where TReturn : class? { var nestedRedirect = _redirect.RedirectSet.GetOrCreate(name); @@ -39,7 +40,27 @@ public INestedRegisterBuilder ThenRegister(string? name, Action(Func decorator) + public INestedRegisterBuilder ThenRedirect(Expression> constraintExpression, Action>? registerAction = null) where TReturn : class? + { + return ThenRedirect(null, constraintExpression, registerAction); + } + + public INestedRegisterBuilder ThenRedirect(string? name, Expression> constraintExpression, Action>? registerAction = null) where TReturn : class? + { + var nestedRedirect = _redirect + .To(constraintExpression) + .ViaRedirect(name, opt => + { + opt.DisableSatisfyStrict(); + opt.Persist(); + }); + + registerAction?.Invoke(new NestedRegisterBuilder(nestedRedirect, _registeredRedirects)); + + return this; + } + + public INestedRegisterBuilder ThenDecorate(Func decorator) { _redirect.ViaDecorator(decorator, opt => { diff --git a/test/DivertR.UnitTests/DiverterTests.cs b/test/DivertR.UnitTests/DiverterTests.cs index 8f535d73..52407ec6 100644 --- a/test/DivertR.UnitTests/DiverterTests.cs +++ b/test/DivertR.UnitTests/DiverterTests.cs @@ -334,7 +334,7 @@ public void AddNamedRedirectByType_ShouldAdd() public void GivenNestedRegistration_WhenRegisteredTypeReturned_ShouldProxy() { // ARRANGE - var diverter = _diverterBuilder.Register(inner => inner.ThenRegister()).Create(); + var diverter = _diverterBuilder.Register(inner => inner.ThenRedirect()).Create(); var fooProxy = diverter.Redirect().Proxy(new Foo()); diverter @@ -349,11 +349,30 @@ public void GivenNestedRegistration_WhenRegisteredTypeReturned_ShouldProxy() bar.Name.ShouldBe("bar redirected"); } + [Fact] + public void GivenNamedNestedRegistration_WhenRegisteredTypeReturned_ShouldProxy() + { + // ARRANGE + var diverter = _diverterBuilder.Register(inner => inner.ThenRedirect("group")).Create(); + var fooProxy = diverter.Redirect().Proxy(new Foo()); + + diverter + .Redirect("group") + .To(x => x.Name) + .Via(call => call.CallNext() + " redirected"); + + // ACT + var bar = fooProxy.EchoGeneric(new Bar("bar")); + + // ASSERT + bar.Name.ShouldBe("bar redirected"); + } + [Fact] public async Task GivenNestedRegistration_WhenRegisteredTaskTypeReturned_ShouldProxy() { // ARRANGE - var diverter = _diverterBuilder.Register(inner => inner.ThenRegister()).Create(); + var diverter = _diverterBuilder.Register(inner => inner.ThenRedirect()).Create(); var fooProxy = diverter.Redirect().Proxy(new Foo()); diverter @@ -372,7 +391,7 @@ public async Task GivenNestedRegistration_WhenRegisteredTaskTypeReturned_ShouldP public async Task GivenNestedRegistration_WhenRegisteredValueTaskTypeReturned_ShouldProxy() { // ARRANGE - var diverter = _diverterBuilder.Register(inner => inner.ThenRegister()).Create(); + var diverter = _diverterBuilder.Register(inner => inner.ThenRedirect()).Create(); var fooProxy = diverter.Redirect().Proxy(new Foo()); diverter @@ -393,8 +412,8 @@ public void GivenNestedNestedRegistration_WhenNestedRegisteredTypeReturned_Shoul // ARRANGE var diverter = _diverterBuilder .Register(x1 => x1 - .ThenRegister(x2 => x2 - .ThenRegister())) + .ThenRedirect(x2 => x2 + .ThenRedirect())) .Create(); var foo = new Foo("inner"); @@ -419,7 +438,7 @@ public void GivenNestedRegistration_WhenSameProxyInstanceReturned_ShouldCache() // ARRANGE var diverter = _diverterBuilder .Register(x => x - .ThenRegister()) + .ThenRedirect()) .Create(); var foo = new Foo("inner"); @@ -436,7 +455,7 @@ public void GivenNestedRegistration_WhenSameProxyInstanceReturned_ShouldCache() public void GivenNestedRegistration_WhenRedirectReset_ShouldPersist() { // ARRANGE - var diverter = _diverterBuilder.Register(x => x.ThenRegister()).Create(); + var diverter = _diverterBuilder.Register(x => x.ThenRedirect()).Create(); var fooProxy = diverter.Redirect().Proxy(new Foo()); diverter.ResetAll(); @@ -462,8 +481,8 @@ public void GivenNestedRegistration_WhenRegisterExistingNestedType_ShouldThrowDi var testAction = () => { _diverterBuilder.Register(x => x - .ThenRegister() - .ThenRegister(y => y.ThenRegister())); + .ThenRedirect() + .ThenRedirect(y => y.ThenRedirect())); }; // ASSERT @@ -474,7 +493,96 @@ public void GivenNestedRegistration_WhenRegisterExistingNestedType_ShouldThrowDi public void GivenNestedRegistration_WhenStrictMode_ShouldDisableSatisfyStrict() { // ARRANGE - var diverter = _diverterBuilder.Register(x => x.ThenRegister()).Create(); + var diverter = _diverterBuilder.Register(x => x.ThenRedirect()).Create(); + + var fooProxy = diverter.Redirect().Proxy(new Foo()); + diverter.Redirect().Strict(); + + // ACT + var testAction = () => fooProxy.EchoGeneric(new Bar("bar")); + + // ASSERT + testAction.ShouldThrow(); + } + + [Fact] + public void GivenNestedRedirect_WhenRedirectTypeReturned_ShouldProxy() + { + // ARRANGE + var diverter = _diverterBuilder + .Register(x => x + .ThenRedirect(foo => foo.EchoGeneric(Is.Any))) + .Create(); + + var fooProxy = diverter.Redirect().Proxy(new Foo()); + + diverter + .Redirect() + .To(x => x.Name) + .Via(call => call.CallNext() + " redirected"); + + // ACT + var bar = fooProxy.EchoGeneric(new Bar("bar")); + + // ASSERT + bar.Name.ShouldBe("bar redirected"); + } + + [Fact] + public void GivenNamedNestedRedirect_WhenRedirectTypeReturned_ShouldProxy() + { + // ARRANGE + var diverter = _diverterBuilder + .Register(foo => foo + .ThenRedirect("group", x => x.EchoGeneric(Is.Any))) + .Create(); + + var fooProxy = diverter.Redirect().Proxy(new Foo()); + + diverter + .Redirect("group") + .To(x => x.Name) + .Via(call => call.CallNext() + " redirected"); + + // ACT + var bar = fooProxy.EchoGeneric(new Bar("bar")); + + // ASSERT + bar.Name.ShouldBe("bar redirected"); + } + + [Fact] + public void GivenNestedRedirect_WhenRedirectReset_ShouldPersist() + { + // ARRANGE + var diverter = _diverterBuilder + .Register(x => x + .ThenRedirect(foo => foo.EchoGeneric(Is.Any))) + .Create(); + + var fooProxy = diverter.Redirect().Proxy(new Foo()); + diverter.ResetAll(); + + diverter + .Redirect() + .To(x => x.Name) + .Via(call => call.CallNext() + " redirected"); + + // ACT + var bar = fooProxy.EchoGeneric(new Bar("bar")); + + // ASSERT + bar.Name.ShouldBe("bar redirected"); + } + + [Fact] + public void GivenNestedRedirect_WhenStrictMode_ShouldDisableSatisfyStrict() + { + // ARRANGE + var diverter = _diverterBuilder + .Register(x => x + .ThenRedirect(foo => foo.EchoGeneric(Is.Any))) + .Create(); var fooProxy = diverter.Redirect().Proxy(new Foo()); diverter.Redirect().Strict(); diff --git a/test/DivertR.WebAppTests/TestHarness/WebAppFixture.cs b/test/DivertR.WebAppTests/TestHarness/WebAppFixture.cs index 7e5dcd42..304997e4 100644 --- a/test/DivertR.WebAppTests/TestHarness/WebAppFixture.cs +++ b/test/DivertR.WebAppTests/TestHarness/WebAppFixture.cs @@ -18,9 +18,9 @@ public class WebAppFixture private readonly IDiverter _diverter = new DiverterBuilder() .Register() .Register(x => x - .ThenRegister()) // Nested registrations allow redirecting inner services created outside DI e.g. by factories + .ThenRedirect()) // Nested registrations allow redirecting inner services not resolved by DI e.g. by factories .Register() - .AddRedirect() // Adds standalone redirect type that will not be installed into IServiceCollection + .AddRedirect() // Attach a standalone (non DI service) redirect for proxying the xUnit ITestOutputHelper logger. .Create(); private readonly WebApplicationFactory _webApplicationFactory;