diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b2ad2889..52c310d909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Enhancements: - Added trace logging level below Debug; maps to Trace in log4net/NLog, and Verbose in Serilog (@pi3k14, #404) - Recognize read-only parameters by the `In` modreq (@zvirja, #406) - DictionaryAdapter: Exposed GetAdapter overloads with NameValueCollection parameter in .NET Standard (@rzontar, #423) +- New `IInvocation.CaptureProceedInfo()` method to enable better implementations of asynchronous interceptors (@stakx, #439) Deprecations: - The API surrounding `Lock` has been deprecated. This consists of the members listed below. Consider using the Base Class Library's `System.Threading.ReaderWriterLockSlim` instead. (@stakx, #391) diff --git a/docs/dynamicproxy-async-interception.md b/docs/dynamicproxy-async-interception.md new file mode 100644 index 0000000000..8b2eb0e175 --- /dev/null +++ b/docs/dynamicproxy-async-interception.md @@ -0,0 +1,290 @@ +# Asynchronous interception + +This article discusses several interception scenarios related to asynchrony. We'll look at these scenarios in increasing order of implementation difficulty: + + * Intercepting awaitable methods that don't produce a return value (e.g. `Task`), with and without using `async`/`await` in interceptor code + + * Intercepting awaitable methods that do produce a return value (e.g. `Task`), with using `async`/`await` in interceptor code + + * Using `invocation.Proceed()` in combination with `async`/`await` + +This article contains C# code examples that make use of the `async`/`await` keywords. Before you proceed, please make sure that you have a good understanding of how these work. They are merely "syntactic sugar" that cause the .NET compilers to rewrite an async method to a state machine with continuations. The [Async/Await FAQ by Stephen Toub](https://devblogs.microsoft.com/pfxteam/asyncawait-faq/) explains these keywords in more detail. + +For brevity's sake, the examples shown in this article will focus on an interceptor's `Intercept` method. Assume that code examples are backed by the following code: + + +```csharp +var serviceProxy = proxyGenerator.CreateInterfaceProxyWithoutTarget(new AsyncInterceptor()); + +// Examples will show how interception gets triggered: +int result = await serviceProxy.GetAsync(); + +public interface IService +{ + Task DoAsync(); + Task GetAsync(int n); +} + +class AsyncInterceptor : IInterceptor +{ + // Examples will mostly focus on this method's implementation: + public void Intercept(IInvocation invocation) + { + ... + } +} +``` + + +## Intercepting awaitable methods that don't produce a return value + +Intercepting awaitable methods (e.g. ones with a return type of `Task`) is fairly easy and not much different from intercepting non-awaitable methods. You'll simply have to set the intercepted invocation's return value to any valid `Task` object. + + +```csharp +// calling code: +await serviceProxy.DoAsync(); + +// interception: +public void Intercept(IInvocation invocation) +{ + invocation.ReturnValue = httpClient.PostAsync(...); +} +``` + + +That wasn't very interesting, and in real-world scenarios you'll quickly reach a point where you'd like to use `async`/`await`. That, however, is also fairly trivial: + + +```csharp +// calling code: +await serviceProxy.DoAsync(); + +// interception: +public void Intercept(IInvocation invocation) +{ + invocation.ReturnValue = InterceptAsync(invocation); +} + +private async Task InterceptAsync(IInvocation invocation) +{ + // In this method, you have the comfort of using `await`: + var response = await httpClient.PostAsync(...); + ... +} +``` + + +Things get more complicated once you want to return a value to the calling code. Let's look at that next! + + +## Intercepting awaitable methods that produce a return value + +When intercepting an awaitable method that produces a return value (such as a method with a return type of `Task`), it is important to remember that the very first `await` that gets hit by execution effectively returns to the caller right away. (The C# compiler will move the statements following the `await` into a continuation that will execute once the `await` "completes".) + +In other words, the very first `await` inside your interceptor completes the proxy method interception! + +DynamicProxy requires that interceptors (or the proxy target object) provide a return value for intercepted non-`void` methods. Naturally, the same requirement holds in an async scenario. Because the first `await` causes an early return to the caller, you must make sure to set the intercepted invocation's return value prior to any `await`. + +We already did that at the end of the last section; let's quickly go back and take a closer look: + + +```csharp +// calling code: +var result = await serviceProxy.DoAsync(); + +// interception: +public void Intercept(IInvocation invocation) +{ + invocation.ReturnValue = InterceptAsync(invocation); + // ^^^^^^^^^^^^^ +} + +private async Task InterceptAsync(IInvocation invocation) +{ + // At this point, interception is still going on + // as we haven't yet hit upon an `await`. + + var response = await httpClient.PostAsync(...); + + // At this point, interception has already completed! + + invocation.ReturnValue = ...; + // ^ Any assignments to `invocation.ReturnValue` are no longer + // observable by the calling code, which already received its + // return value earlier (specifically, the `Task` produced by + // the C# compiler representing this asynchronous method). + // `invocation` is essentially a "cold", "detached", "stale", + // or "dead" object (pick your favourite term). +} +``` + + +So, how would we communicate a return value back to the calling code when the intercepted method's return type is not just `Task`, but say, `Task`? The following example shows how you can control the result of the task received by calling code: + + +```csharp +// calling code: +var result = await serviceProxy.GetAsync(41); +Assert.Equal(42, result); + +// interception: +public void Intercept(IInvocation invocation) +{ + invocation.ReturnValue = InterceptAsync(invocation); +} + +private async Task InterceptAsync(IInvocation invocation) +{ + // We can still use `await`: + await ...; + + // And we can simply `return` values, and the C# compiler + // will do the rest for us: + return (int)invocation.Arguments[0] + 1; +} +``` + + +Unfortunately, it usually won't be *that* easy in real-world scenarios, where you cannot assume that every method that your interceptor deals with will have the exact same return type of `Task`. So, instead of being able to just comfortably `return someInt;` from a `async Task` method, you'll have to resort to a non-generic `Task`, a `TaskCompletionSource`, and some reflection: + + +```csharp +// calling code--as before: +var result = await serviceProxy.GetAsync(41); +Assert.Equal(42, result); + +// interception: +public void Intercept(IInvocation invocation) +{ + var returnType = invocation.Method.ReturnType; + + // For this example, we'll just assume that we're dealing with + // a `returnType` of `Task`. In practice, you'd have + // to have logic for non-`Task`, `Task`, `Task`, and + // any other awaitable types that you care about: + Debug.Assert(typeof(Task).IsAssignableFrom(returnType) && returnType.IsGenericType); + + // Instantiate a `TaskCompletionSource`, whose `Task` + // we will return to the calling code, so that we can control + // the result: + var tcsType = typeof(TaskCompletionSource<>) + .MakeGenericType(returnType.GetGenericArguments()[0]); + var tcs = Activator.CreateInstance(tcsType); + invocation.ReturnValue = tcsType.GetProperty("Task").GetValue(tcs, null); + + // Because we're not in an `async` method, we cannot use `await` + // and have the compiler generate a continuation for the code + // following it. Let's therefore set up the continuation manually: + InterceptAsync(invocation).ContinueWith(_ => + { + // This sets the result of the task that we have previously + // returned to the calling code, based on `invocation.ReturnValue` + // which has been set in the (by now completed) `InterceptAsync` + // method (see below): + tcsType.GetMethod("SetResult").Invoke(tcs, new object[] { invocation.ReturnValue }); + }); +} + +private async Task InterceptAsync(IInvocation invocation) +{ + // In this method, we now have the comfort of `await`: + var response = await httpClient.GetStringAsync(...); + + // ... and we can still set the final return value! Note that + // the return type of this method is now `Task`, not `Task`, + // so we can no longer `return` a value. Instead, we use the + // "stale" `invocation` to hold the real return value for us. + // It will get processed in the continuation (above) when this + // async method completes: + invocation.ReturnValue = (int)invocation.Arguments[0] + 1; +} +``` + + +Phew! And things get even more complex once we want to do an `invocation.Proceed()` to a succeeding interceptor or the proxy's target method. Let's look at that next! + + +## Using `invocation.Proceed()` in combination with `async`/`await` + +Here's a quick recap about `invocation.Proceed()`: This method gets used to proceed to the next interceptor in line, or, if there is no other interceptor but a proxy target object, to that. Remember this image from the [introduction to DynamicProxy](dynamicproxy-introduction.md#interception-pipeline): + +![](images/proxy-pipeline.png) + +Let's get straight to the point: `Proceed()` will not do what you might expect in an async scenario! Remember that the very first `await` inside your interceptor completes interception, i.e. causes an early return to the calling code? This means that interception starts to "bubble back up" towards the calling code (i.e. along the green arrows in the above picture). + +Therefore, after having `await`-ed in your interceptor, interception has completed at the position of the last green arrow. Calling `invocation.Proceed` in the continuation (i.e. after the `await`) will then simply advance to the very first interceptor again... that's likely not what you want, and can cause infinite loops and other unexpected malfunctions. + + +```csharp +// calling code: +await serviceProxy.DoAsync(); + +// interception: +public void Intercept(IInvocation invocation) +{ + invocation.ReturnValue = InterceptAsync(invocation); +} + +private async Task InterceptAsync(IInvocation invocation) +{ + await ...; + invocation.Proceed(); // will not proceed, but reenter this interceptor + // or--if this isn't the first--an earlier one! +} +``` + + +In order for you to be able to work around this problem, DynamicProxy offers another method, `invocation.CaptureProceedInfo()`, which allows you to capture the invocation's current position in the interception pipeline (i.e. where along the yellow arrows it is currently located). This method returns an object to you which you can use to continue interception from this very same location onward. + +The solution to the problem demonstrated above now becomes very simple: + + +```csharp +// calling code--as before: +await serviceProxy.DoAsync(); + +// interception: +public void Intercept(IInvocation invocation) +{ + invocation.ReturnValue = InterceptAsync(invocation); +} + +private async Task InterceptAsync(IInvocation invocation) +{ + // If we want to `Proceed` at any point in this method, + // we need to capture how far in the invocation pipeline + // we're currently located *before* we `await`: + var proceed = invocation.CaptureProceedInfo(); + + await ...; + + // At this point, interception is completed and we have + // a "stale" invocation that has been reset to the very + // beginning of the interception pipeline. However, + // that doesn't mean that we cannot send it onward to the + // remaining interceptors or to the proxy target object: + proceed.Invoke(); + + // At this point, a later interceptor might have over- + // written `invocation.ReturnValue`. As explained earlier, + // while the calling code will no longer observe this + // value, we could inspect it to set our own task's + // result (if we returned a `Task`). +} +``` + + +## Closing remarks + + * As you have seen, async interception is only trivial in very simple scenarios, but can get quite complex very quickly. If you don't feel comfortable writing such code yourself, look for third-party libraries that help you with async interception, for example: + + * [Castle.Core.AsyncInterceptor](https://www.nuget.org/packages/Castle.Core.AsyncInterceptor) (third-party, despite the Castle Project's package namespace) + + If you are the author of a generally useful async interception helper library, and would like to add your library to the above list, feel free to submit a PR. + + * The above examples have shown cases where a "stale" invocation object has its `ReturnValue` repeatedly set, even though only the first value might be observed by calling code. It is however possible that invocation objects are recorded somewhere, and could be inspected later on. Other parties might then observe a `ReturnValue` that does not reflect what the original caller got. + + Because of that, it might be good practice for your async interceptors to restore, at the end of interception, the `ReturnValue` to the value that actually made it back to the calling code. + + * The same recommendation—restoration to the value(s) observed by the calling code—applies to by-ref arguments that have been repeatedly overwritten using `invocation.SetArgumentValue`. diff --git a/docs/dynamicproxy.md b/docs/dynamicproxy.md index 58bc754708..ac8a4bcab6 100644 --- a/docs/dynamicproxy.md +++ b/docs/dynamicproxy.md @@ -20,6 +20,7 @@ If you're new to DynamicProxy you can read a [quick introduction](dynamicproxy-i * [SRP applies to interceptors](dynamicproxy-srp-applies-to-interceptors.md) * [Behavior of by-reference parameters during interception](dynamicproxy-by-ref-parameters.md) * [Optional parameter value limitations](dynamicproxy-optional-parameter-value-limitations.md) +* [Asynchronous interception](dynamicproxy-async-interception.md) :information_source: **Where is `Castle.DynamicProxy.dll`?:** DynamicProxy used to live in its own assembly. As part of changes in version 2.5 it was merged into `Castle.Core.dll` and that's where you'll find it. diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/AsyncInterceptorTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/AsyncInterceptorTestCase.cs new file mode 100644 index 0000000000..76c1fc7d7e --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/AsyncInterceptorTestCase.cs @@ -0,0 +1,64 @@ +// Copyright 2004-2019 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests +{ + using System.Threading.Tasks; + + using Castle.DynamicProxy.Tests.Classes; + using Castle.DynamicProxy.Tests.Interfaces; + + using NUnit.Framework; + + [TestFixture] + public class AsyncInterceptorTestCase : BasePEVerifyTestCase + { + [Test] + public async Task Should_Intercept_Asynchronous_Methods_With_An_Async_Operations_Prior_To_Calling_Proceed() + { + // Arrange + IInterfaceWithAsynchronousMethod target = new ClassWithAsynchronousMethod(); + IInterceptor interceptor = new AsyncInterceptor(); + + IInterfaceWithAsynchronousMethod proxy = + generator.CreateInterfaceProxyWithTargetInterface(target, interceptor); + + // Act + await proxy.Method().ConfigureAwait(false); + } + + private class AsyncInterceptor : IInterceptor + { + public void Intercept(IInvocation invocation) + { + invocation.ReturnValue = InterceptAsyncMethod(invocation); + } + + private static async Task InterceptAsyncMethod(IInvocation invocation) + { + var proceed = invocation.CaptureProceedInfo(); + + await Task.Delay(10).ConfigureAwait(false); + + proceed.Invoke(); + + // Return value is being set in two situations, but this doesn't matter + // for the above test. + Task returnValue = (Task)invocation.ReturnValue; + + await returnValue.ConfigureAwait(false); + } + } + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithAsynchronousMethod.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithAsynchronousMethod.cs new file mode 100644 index 0000000000..abb7cbd4f3 --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithAsynchronousMethod.cs @@ -0,0 +1,38 @@ +// Copyright 2004-2019 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests.Classes +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + using Castle.DynamicProxy.Tests.Interfaces; + + public class ClassWithAsynchronousMethod : IInterfaceWithAsynchronousMethod + { + public async Task Method() + { + Console.WriteLine( + $"Before Await ClassWithAsynchronousMethod:Method ThreadId='{Thread.CurrentThread.ManagedThreadId}'.", + Thread.CurrentThread.ManagedThreadId); + + await Task.Delay(10).ConfigureAwait(false); + + Console.WriteLine( + $"After Await ClassWithAsynchronousMethod:Method ThreadId='{Thread.CurrentThread.ManagedThreadId}'.", + Thread.CurrentThread.ManagedThreadId); + } + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/IInterfaceWithAsynchronousMethod.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/IInterfaceWithAsynchronousMethod.cs new file mode 100644 index 0000000000..52babefbf5 --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/IInterfaceWithAsynchronousMethod.cs @@ -0,0 +1,23 @@ +// Copyright 2004-2019 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests.Interfaces +{ + using System.Threading.Tasks; + + public interface IInterfaceWithAsynchronousMethod + { + Task Method(); + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/InvocationProceedInfoTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/InvocationProceedInfoTestCase.cs new file mode 100644 index 0000000000..c28d3431bf --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/InvocationProceedInfoTestCase.cs @@ -0,0 +1,226 @@ +// Copyright 2004-2019 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Castle.DynamicProxy.Tests.Interceptors; + using Castle.DynamicProxy.Tests.InterClasses; + using Castle.DynamicProxy.Tests.Interfaces; + + using NUnit.Framework; + + [TestFixture] + public class InvocationProceedInfoTestCase + { + private readonly ProxyGenerator generator = new ProxyGenerator(); + + [Test] + public void Proxy_without_target_and_last_interceptor_ProceedInfo_succeeds() + { + var interceptor = new WithCallbackInterceptor(invocation => + { + invocation.ReturnValue = 0; // not relevant to this test, but prevents DP + // from complaining about missing return value. + var proceed = invocation.CaptureProceedInfo(); + }); + + var proxy = generator.CreateInterfaceProxyWithoutTarget(interceptor); + proxy.OneMethod(); + } + + [Test] + public void Proxy_without_target_and_last_interceptor_ProceedInfo_Invoke_throws_NotImplementedException() + { + var interceptor = new WithCallbackInterceptor(invocation => + { + invocation.ReturnValue = 0; // not relevant for this test, but prevents DP + // from complaining about missing return value. + var proceed = invocation.CaptureProceedInfo(); + Assert.Throws(() => proceed.Invoke()); + }); + + var proxy = generator.CreateInterfaceProxyWithoutTarget(interceptor); + proxy.OneMethod(); + } + + [Test] + public void Proxy_without_target_and_second_to_last_interceptor_ProceedInfo_Invoke_proceeds_to_interceptor() + { + var interceptors = new IInterceptor[] + { + new WithCallbackInterceptor(invocation => + { + var proceed = invocation.CaptureProceedInfo(); + proceed.Invoke(); + }), + new SetReturnValueInterceptor(1), + }; + + var proxy = generator.CreateInterfaceProxyWithoutTarget(interceptors); + var returnValue = proxy.OneMethod(); + Assert.AreEqual(1, returnValue); + } + + [Test] + public void Proxy_with_target_and_last_interceptor_ProceedInfo_Invoke_proceeds_to_target() + { + var target = new One(); + + var interceptor = new WithCallbackInterceptor(invocation => + { + var proceed = invocation.CaptureProceedInfo(); + proceed.Invoke(); + }); + + var proxy = generator.CreateInterfaceProxyWithTarget(new One(), interceptor); + var returnValue = proxy.OneMethod(); + Assert.AreEqual(1, returnValue); + } + + [Test] + public void CaptureProceedInfo_returns_a_new_object_every_time() + { + var interceptor = new WithCallbackInterceptor(invocation => + { + invocation.ReturnValue = 0; // not relevant to this test, but prevents DP + // from complaining about missing return value. + var proceed1 = invocation.CaptureProceedInfo(); + var proceed2 = invocation.CaptureProceedInfo(); + Assert.AreNotSame(proceed2, proceed1); + }); + + var proxy = generator.CreateInterfaceProxyWithoutTarget(interceptor); + _ = proxy.OneMethod(); + } + + [Test] + public void ProceedInfo_Invoke_proceeds_to_same_interceptor_on_repeated_calls() + { + var secondInterceptorInvokeCount = 0; + var thirdInterceptorInvokeCount = 0; + + var interceptors = new IInterceptor[] + { + new WithCallbackInterceptor(invocation => + { + invocation.ReturnValue = 0; // not relevant to this test, but prevents DP + // from complaining about missing return value. + + var proceed = invocation.CaptureProceedInfo(); + proceed.Invoke(); + proceed.Invoke(); + }), + new WithCallbackInterceptor(invocation => + { + secondInterceptorInvokeCount++; + }), + new WithCallbackInterceptor(invocation => + { + thirdInterceptorInvokeCount++; + }), + }; + + var proxy = generator.CreateInterfaceProxyWithoutTarget(interceptors); + _ = proxy.OneMethod(); + Assert.AreEqual(2, secondInterceptorInvokeCount); + Assert.AreEqual(0, thirdInterceptorInvokeCount); + } + + [Test] + public void ProceedInfo_Invoke_of_different_instances_captured_at_same_time_proceed_to_same_interceptor() + { + // The second-to-last test above established that `CaptureProceedInfo` + // returns a different object every time. The following test established + // that `proceed.Invoke` proceeds to the same place every time. Let's now + // combine these and see whether two different `proceedN.Invoke` *still* + // reach the same place. (Conceptually, this is sort of a value equality + // test for `ProceedInfo` objects.) + + var secondInterceptorInvokeCount = 0; + var thirdInterceptorInvokeCount = 0; + + var interceptors = new IInterceptor[] + { + new WithCallbackInterceptor(invocation => + { + invocation.ReturnValue = 0; // not relevant to this test, but prevents DP + // from complaining about missing return value. + + var proceed1 = invocation.CaptureProceedInfo(); + var proceed2 = invocation.CaptureProceedInfo(); + Assume.That(proceed1 != proceed2); + + proceed1.Invoke(); + proceed2.Invoke(); + }), + new WithCallbackInterceptor(invocation => + { + secondInterceptorInvokeCount++; + }), + new WithCallbackInterceptor(invocation => + { + thirdInterceptorInvokeCount++; + }), + }; + + var proxy = generator.CreateInterfaceProxyWithoutTarget(interceptors); + _ = proxy.OneMethod(); + Assert.AreEqual(2, secondInterceptorInvokeCount); + Assert.AreEqual(0, thirdInterceptorInvokeCount); + } + + [Test] + public void Proxy_with_several_interceptors_ProceedInfo_Invoke_proceeds_in_correct_order() + { + // At various points during interception, we will record a number in this list. + // These numbers represent the expected order in which the statements should be hit. + // See the documented examples below. + var breadcrumbs = new List(); + + var interceptors = new IInterceptor[] + { + new WithCallbackInterceptor(invocation => + { + breadcrumbs.Add(1); // (This statement is expected to be the first one + // recorded, because it has the smallest number.) + var proceed = invocation.CaptureProceedInfo(); + proceed.Invoke(); + breadcrumbs.Add(5); // (This statement is expected to be the last one + // recorded, because it has the largest number.) + }), + new WithCallbackInterceptor(invocation => + { + breadcrumbs.Add(2); + var proceed = invocation.CaptureProceedInfo(); + proceed.Invoke(); + breadcrumbs.Add(4); + }), + new WithCallbackInterceptor(invocation => + { + breadcrumbs.Add(3); + invocation.ReturnValue = 42; + }), + }; + + var proxy = generator.CreateInterfaceProxyWithoutTarget(interceptors); + var returnValue = proxy.OneMethod(); + Assert.AreEqual(42, returnValue); + CollectionAssert.AreEqual(Enumerable.Range(1, 5), breadcrumbs); + } + } +} diff --git a/src/Castle.Core/DynamicProxy/AbstractInvocation.cs b/src/Castle.Core/DynamicProxy/AbstractInvocation.cs index d8d55adf65..3ec94d0f38 100644 --- a/src/Castle.Core/DynamicProxy/AbstractInvocation.cs +++ b/src/Castle.Core/DynamicProxy/AbstractInvocation.cs @@ -142,6 +142,11 @@ public void Proceed() } } + public IInvocationProceedInfo CaptureProceedInfo() + { + return new ProceedInfo(this); + } + protected abstract void InvokeMethodOnTarget(); protected void ThrowOnNoTarget() @@ -188,5 +193,31 @@ private MethodInfo EnsureClosedMethod(MethodInfo method) } return method; } + + private sealed class ProceedInfo : IInvocationProceedInfo + { + private readonly AbstractInvocation invocation; + private readonly int interceptorIndex; + + public ProceedInfo(AbstractInvocation invocation) + { + this.invocation = invocation; + this.interceptorIndex = invocation.currentInterceptorIndex; + } + + public void Invoke() + { + var previousInterceptorIndex = invocation.currentInterceptorIndex; + try + { + invocation.currentInterceptorIndex = interceptorIndex; + invocation.Proceed(); + } + finally + { + invocation.currentInterceptorIndex = previousInterceptorIndex; + } + } + } } } \ No newline at end of file diff --git a/src/Castle.Core/DynamicProxy/IInvocation.cs b/src/Castle.Core/DynamicProxy/IInvocation.cs index 35563d3235..1a98e6befc 100644 --- a/src/Castle.Core/DynamicProxy/IInvocation.cs +++ b/src/Castle.Core/DynamicProxy/IInvocation.cs @@ -114,6 +114,12 @@ public interface IInvocation /// void Proceed(); + /// + /// Returns an object describing the operation for this + /// at this specific point during interception. + /// + IInvocationProceedInfo CaptureProceedInfo(); + /// /// Overrides the value of an argument at the given with the /// new provided. diff --git a/src/Castle.Core/DynamicProxy/IInvocationProceedInfo.cs b/src/Castle.Core/DynamicProxy/IInvocationProceedInfo.cs new file mode 100644 index 0000000000..9fe3846c0f --- /dev/null +++ b/src/Castle.Core/DynamicProxy/IInvocationProceedInfo.cs @@ -0,0 +1,31 @@ +// Copyright 2004-2019 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy +{ + using System; + + /// + /// Describes the operation for an + /// at a specific point during interception. + /// + public interface IInvocationProceedInfo + { + /// + /// Executes the operation described by this instance. + /// + /// There is no interceptor, nor a proxy target object, to proceed to. + void Invoke(); + } +}