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..eb190de02a --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/AsyncInterceptorTestCase.cs @@ -0,0 +1,48 @@ +// Copyright 2004-2016 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 CastleTests.DynamicProxy.Tests +{ + using System; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + + using Castle.DynamicProxy; + using Castle.DynamicProxy.Tests; + + using CastleTests.DynamicProxy.Tests.Classes; + using CastleTests.DynamicProxy.Tests.Interfaces; + using CastleTests.Interceptors; + + 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); + } + } +} 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..0d7cb439ed --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithAsynchronousMethod.cs @@ -0,0 +1,38 @@ +// Copyright 2004-2010 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 CastleTests.DynamicProxy.Tests.Classes +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + using CastleTests.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..724bcc5286 --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/IInterfaceWithAsynchronousMethod.cs @@ -0,0 +1,23 @@ +// Copyright 2004-2010 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 CastleTests.DynamicProxy.Tests.Interfaces +{ + using System.Threading.Tasks; + + public interface IInterfaceWithAsynchronousMethod + { + Task Method(); + } +} diff --git a/src/Castle.Core.Tests/Interceptors/AsyncInterceptor.cs b/src/Castle.Core.Tests/Interceptors/AsyncInterceptor.cs new file mode 100644 index 0000000000..1609bae0ec --- /dev/null +++ b/src/Castle.Core.Tests/Interceptors/AsyncInterceptor.cs @@ -0,0 +1,45 @@ +// Copyright 2004-2010 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 CastleTests.Interceptors +{ + using System.Threading.Tasks; + using Castle.DynamicProxy; + + public class AsyncInterceptor : IInterceptor + { + public void Intercept(IInvocation invocation) + { + invocation.ReturnValue = InterceptAsyncMethod(invocation); + } + + private static async Task InterceptAsyncMethod(IInvocation invocation) + { + var interceptionProgress = invocation.GetMemento(InvocationMementoOptions.InterceptionProgress); + + // It all falls down when executing async before calling Proceed(). + await Task.Delay(10).ConfigureAwait(false); + + interceptionProgress.Restore(); + + invocation.Proceed(); + + // Hmmmmm, now with it simplified down to this, I see the glaring hole that is the return value being set + // in two situations. + Task returnValue = (Task)invocation.ReturnValue; + + await returnValue.ConfigureAwait(false); + } + } +} diff --git a/src/Castle.Core/DynamicProxy/AbstractInvocation.cs b/src/Castle.Core/DynamicProxy/AbstractInvocation.cs index d8d55adf65..a409bee84f 100644 --- a/src/Castle.Core/DynamicProxy/AbstractInvocation.cs +++ b/src/Castle.Core/DynamicProxy/AbstractInvocation.cs @@ -142,6 +142,11 @@ public void Proceed() } } + public IInvocationMemento GetMemento(InvocationMementoOptions options) + { + return new Memento(this, options); + } + protected abstract void InvokeMethodOnTarget(); protected void ThrowOnNoTarget() @@ -188,5 +193,56 @@ private MethodInfo EnsureClosedMethod(MethodInfo method) } return method; } + + private sealed class Memento : IInvocationMemento + { + private readonly AbstractInvocation invocation; + private readonly InvocationMementoOptions options; + + private readonly object[] arguments; + private readonly object returnValue; + private readonly int interceptorIndex; + + public Memento(AbstractInvocation invocation, InvocationMementoOptions options) + { + this.invocation = invocation; + this.options = options; + + if ((options & InvocationMementoOptions.Arguments) != 0) + { + var argumentCount = invocation.arguments.Length; + this.arguments = new object[argumentCount]; + Array.Copy(invocation.arguments, this.arguments, argumentCount); + } + + if ((options & InvocationMementoOptions.ReturnValue) != 0) + { + this.returnValue = invocation.ReturnValue; + } + + if ((options & InvocationMementoOptions.InterceptionProgress) != 0) + { + this.interceptorIndex = invocation.currentInterceptorIndex; + } + } + + public void Restore() + { + if ((options & InvocationMementoOptions.Arguments) != 0) + { + Array.Copy(arguments, invocation.arguments, arguments.Length); + } + + if ((options & InvocationMementoOptions.ReturnValue) != 0) + { + invocation.ReturnValue = returnValue; + } + + if ((options & InvocationMementoOptions.InterceptionProgress) != 0) + { + invocation.currentInterceptorIndex = interceptorIndex; + } + } + } } } \ No newline at end of file diff --git a/src/Castle.Core/DynamicProxy/IInvocation.cs b/src/Castle.Core/DynamicProxy/IInvocation.cs index 35563d3235..76cd78ace5 100644 --- a/src/Castle.Core/DynamicProxy/IInvocation.cs +++ b/src/Castle.Core/DynamicProxy/IInvocation.cs @@ -125,5 +125,11 @@ public interface IInvocation /// The index of the argument to override. /// The new value for the argument. void SetArgumentValue(int index, object value); + + /// + /// Gets an object that captures the specified parts of this invocation's state for later restoration. + /// + /// Specifies which parts of this invocation's state should be captured. + IInvocationMemento GetMemento(InvocationMementoOptions options); } } \ No newline at end of file diff --git a/src/Castle.Core/DynamicProxy/IInvocationMemento.cs b/src/Castle.Core/DynamicProxy/IInvocationMemento.cs new file mode 100644 index 0000000000..048bb1371e --- /dev/null +++ b/src/Castle.Core/DynamicProxy/IInvocationMemento.cs @@ -0,0 +1,30 @@ +// 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.ComponentModel; + + /// + /// Represents a full or partial state of an captured at an earlier time. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public interface IInvocationMemento + { + /// + /// Restores the state captured by this instance to the associated . + /// + void Restore(); + } +} diff --git a/src/Castle.Core/DynamicProxy/InvocationMementoOptions.cs b/src/Castle.Core/DynamicProxy/InvocationMementoOptions.cs new file mode 100644 index 0000000000..e3522b4e0d --- /dev/null +++ b/src/Castle.Core/DynamicProxy/InvocationMementoOptions.cs @@ -0,0 +1,46 @@ +// 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; + + /// + /// Specifies which parts of an invocation's state to capture. + /// + [Flags] + public enum InvocationMementoOptions + { + /// + /// Specifies that arguments should be captured. + /// + Arguments = 1, + + /// + /// Specifies that the return value should be captured. + /// + ReturnValue = 2, + + /// + /// Specifies that the invocation's position in the interception pipeline should be captured. + /// + /// Note that interception of an invocation may have finished by the time a call to + /// is made. While such a "stale" invocation + /// can be procesed again by interceptors, setting the return value or by-ref arguments + /// will no longer have any effect on the calling code. + /// + /// + InterceptionProgress = 4, + } +}