From b7f961c8eb0bc52fba7ae037a04fea9826baee90 Mon Sep 17 00:00:00 2001 From: Arnaud Debaene Date: Wed, 4 Jan 2023 13:03:31 +0100 Subject: [PATCH 1/2] added support for methods returning IAsyncEnumerable --- .../AsyncDeterminationInterceptor.cs | 37 +++++++++++- .../AsyncInterceptorBase.cs | 6 ++ .../Castle.Core.AsyncInterceptor.csproj | 26 +++++++- .../IAsyncInterceptor.cs | 7 +++ .../ProcessingAsyncInterceptor.cs | 25 ++++++++ .../AsyncInterceptorShould.cs | 56 ++++++++++++++++++ .../AsyncTimingInterceptorShould.cs | 58 ++++++++++++++++++ .../Castle.Core.AsyncInterceptor.Tests.csproj | 2 +- .../ClassWithAlwaysCompletedAsync.cs | 15 +++++ .../ClassWithAlwaysIncompleteAsync.cs | 15 +++++ .../ClassWithInterfaceToProxy.cs | 30 ++++++++++ .../InterfaceProxies/IInterfaceToProxy.cs | 6 ++ .../InterfaceProxies/TestAsyncInterceptor.cs | 18 ++++++ .../ProcessingAsyncInterceptorShould.cs | 59 +++++++++++++++++++ 14 files changed, 355 insertions(+), 5 deletions(-) diff --git a/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs b/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs index 19cbc67..4841f27 100644 --- a/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs +++ b/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs @@ -16,6 +16,9 @@ public class AsyncDeterminationInterceptor : IInterceptor typeof(AsyncDeterminationInterceptor) .GetMethod(nameof(HandleAsyncWithResult), BindingFlags.Static | BindingFlags.NonPublic)!; + private static readonly MethodInfo HandleAsyncEnumerableInfo = + typeof(AsyncDeterminationInterceptor).GetMethod(nameof(HandleAsyncEnumerable), BindingFlags.Static | BindingFlags.NonPublic)!; + private static readonly ConcurrentDictionary GenericAsyncHandlers = new(); /// @@ -34,6 +37,7 @@ private enum MethodType Synchronous, AsyncAction, AsyncFunction, + AsyncEnumerableFunction, } /// @@ -56,6 +60,7 @@ public virtual void Intercept(IInvocation invocation) AsyncInterceptor.InterceptAsynchronous(invocation); return; case MethodType.AsyncFunction: + case MethodType.AsyncEnumerableFunction: GetHandler(invocation.Method.ReturnType).Invoke(invocation, AsyncInterceptor); return; case MethodType.Synchronous: @@ -70,6 +75,11 @@ public virtual void Intercept(IInvocation invocation) /// private static MethodType GetMethodType(Type returnType) { + TypeInfo typeInfo = returnType.GetTypeInfo(); + + if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + return MethodType.AsyncEnumerableFunction; + // If there's no return type, or it's not a task, then assume it's a synchronous method. if (returnType == typeof(void) || !typeof(Task).IsAssignableFrom(returnType)) return MethodType.Synchronous; @@ -92,9 +102,21 @@ private static GenericAsyncHandler GetHandler(Type returnType) /// private static GenericAsyncHandler CreateHandler(Type returnType) { - Type taskReturnType = returnType.GetGenericArguments()[0]; - MethodInfo method = HandleAsyncMethodInfo.MakeGenericMethod(taskReturnType); - return (GenericAsyncHandler)method.CreateDelegate(typeof(GenericAsyncHandler)); + Type genericType = returnType.GetGenericTypeDefinition(); + if (typeof(Task).IsAssignableFrom(genericType)) + { + Type? taskReturnType = returnType.GetGenericArguments()[0]; + MethodInfo method = HandleAsyncMethodInfo.MakeGenericMethod(taskReturnType); + return (GenericAsyncHandler)method.CreateDelegate(typeof(GenericAsyncHandler)); + } + else if (genericType == typeof(IAsyncEnumerable<>)) + { + Type enumerableType = returnType.GetGenericArguments()[0]; + MethodInfo method = HandleAsyncEnumerableInfo.MakeGenericMethod(enumerableType); + return (GenericAsyncHandler)method.CreateDelegate(typeof(GenericAsyncHandler)); + } + + throw new ArgumentException(nameof(returnType)); } /// @@ -107,4 +129,13 @@ private static void HandleAsyncWithResult(IInvocation invocation, IAsyn { asyncInterceptor.InterceptAsynchronous(invocation); } + + /// + /// This method is created as a delegate and used to make the call to the generic + /// method. + /// + /// The type of the of the method + /// . + private static void HandleAsyncEnumerable(IInvocation invocation, IAsyncInterceptor asyncInterceptor) + => asyncInterceptor.InterceptAsyncEnumerable(invocation); } diff --git a/src/Castle.Core.AsyncInterceptor/AsyncInterceptorBase.cs b/src/Castle.Core.AsyncInterceptor/AsyncInterceptorBase.cs index 7690d45..425119b 100644 --- a/src/Castle.Core.AsyncInterceptor/AsyncInterceptorBase.cs +++ b/src/Castle.Core.AsyncInterceptor/AsyncInterceptorBase.cs @@ -66,6 +66,12 @@ public void InterceptAsynchronous(IInvocation invocation) InterceptAsync(invocation, invocation.CaptureProceedInfo(), ProceedAsynchronous); } + /// + public void InterceptAsyncEnumerable(IInvocation invocation) + { + throw new NotImplementedException(); // TODO : discuss the correct model here + } + /// /// Override in derived classes to intercept method invocations. /// diff --git a/src/Castle.Core.AsyncInterceptor/Castle.Core.AsyncInterceptor.csproj b/src/Castle.Core.AsyncInterceptor/Castle.Core.AsyncInterceptor.csproj index f74a9fe..5fbe573 100644 --- a/src/Castle.Core.AsyncInterceptor/Castle.Core.AsyncInterceptor.csproj +++ b/src/Castle.Core.AsyncInterceptor/Castle.Core.AsyncInterceptor.csproj @@ -1,7 +1,7 @@ - net45;netstandard2.0;net5.0;net6.0;net7.0 + netstandard2.0;net5.0;net6.0;net7.0 Castle.DynamicProxy true false @@ -49,4 +49,28 @@ + + + 7.0.0 + + + + + + 7.0.0 + + + + + + 7.0.0 + + + + + + 7.0.0 + + + diff --git a/src/Castle.Core.AsyncInterceptor/IAsyncInterceptor.cs b/src/Castle.Core.AsyncInterceptor/IAsyncInterceptor.cs index d3a2552..557d616 100644 --- a/src/Castle.Core.AsyncInterceptor/IAsyncInterceptor.cs +++ b/src/Castle.Core.AsyncInterceptor/IAsyncInterceptor.cs @@ -26,4 +26,11 @@ public interface IAsyncInterceptor /// The type of the . /// The method invocation. void InterceptAsynchronous(IInvocation invocation); + + /// + /// Intercepts a method that returns an . + /// + /// The type of the returned enumerable. + /// The method invocation. + void InterceptAsyncEnumerable(IInvocation invocation); } diff --git a/src/Castle.Core.AsyncInterceptor/ProcessingAsyncInterceptor.cs b/src/Castle.Core.AsyncInterceptor/ProcessingAsyncInterceptor.cs index 487b05f..a4db557 100644 --- a/src/Castle.Core.AsyncInterceptor/ProcessingAsyncInterceptor.cs +++ b/src/Castle.Core.AsyncInterceptor/ProcessingAsyncInterceptor.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. namespace Castle.DynamicProxy; +using System.Collections.Generic; /// /// A base type for an which executes only minimal processing when intercepting a @@ -47,6 +48,18 @@ public void InterceptAsynchronous(IInvocation invocation) invocation.ReturnValue = SignalWhenCompleteAsync(invocation, state); } + /// + /// Intercepts a method with return type of . + /// + /// The type of ther returned enumerable. + /// The method invocation. + public void InterceptAsyncEnumerable(IInvocation invocation) + { + TState state = Proceed(invocation); + var innerAsync = (IAsyncEnumerable)invocation.ReturnValue; + invocation.ReturnValue = SignalWhenEnumerationCompleteAsync(invocation, innerAsync, state); + } + /// /// Override in derived classes to receive signals prior method . /// @@ -140,4 +153,16 @@ private async Task SignalWhenCompleteAsync(IInvocation invocat return result; } + + private async IAsyncEnumerable SignalWhenEnumerationCompleteAsync(IInvocation invocation, IAsyncEnumerable innerAsync, TState state) + { + // loop / yield the proxied method + await foreach (TResult item in innerAsync) + { + yield return item; + } + + // Signal that the invocation has been completed. + CompletedInvocation(invocation, state); + } } diff --git a/test/Castle.Core.AsyncInterceptor.Tests/AsyncInterceptorShould.cs b/test/Castle.Core.AsyncInterceptor.Tests/AsyncInterceptorShould.cs index 7b5b191..12333d5 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/AsyncInterceptorShould.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/AsyncInterceptorShould.cs @@ -225,3 +225,59 @@ public async Task ShouldAllowInterceptionAfterInvocation() Assert.Equal($"{MethodName}:InterceptEnd", _log[3]); } } + +public class WhenInterceptingAsyncEnumerableMethods +{ + private const string MethodName = nameof(IInterfaceToProxy.AsyncEnumerableMethod); + private readonly ListLogger _log; + private readonly IInterfaceToProxy _proxy; + + public WhenInterceptingAsyncEnumerableMethods(ITestOutputHelper output) + { + _log = new ListLogger(output); + _proxy = ProxyGen.CreateProxy(_log, new TestAsyncInterceptor(_log)); + } + + [Fact] + public async Task ShouldLog4Entries() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal(10, results.Count); + Assert.Equal(4, _log.Count); + } + + [Fact] + public async Task ShouldAllowInterceptionPriorToInvocation() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal($"{MethodName}:InterceptStart", _log[0]); + } + + [Fact] + public async Task ShouldAllowInterceptionAfterInvocation() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal($"{MethodName}:InterceptEnd", _log[3]); + } +} diff --git a/test/Castle.Core.AsyncInterceptor.Tests/AsyncTimingInterceptorShould.cs b/test/Castle.Core.AsyncInterceptor.Tests/AsyncTimingInterceptorShould.cs index d661b76..3317108 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/AsyncTimingInterceptorShould.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/AsyncTimingInterceptorShould.cs @@ -187,3 +187,61 @@ public async Task ShouldAllowTimingAfterInvocation() Assert.Equal($"{MethodName}:CompletedTiming:{_interceptor.Stopwatch.Elapsed:g}", _log[3]); } } + +public class WhenTimingAsyncEnumerableMethods +{ + private const string MethodName = nameof(IInterfaceToProxy.AsyncEnumerableMethod); + private readonly ListLogger _log; + private readonly TestAsyncTimingInterceptor _interceptor; + private readonly IInterfaceToProxy _proxy; + + public WhenTimingAsyncEnumerableMethods(ITestOutputHelper output) + { + _log = new ListLogger(output); + _interceptor = new TestAsyncTimingInterceptor(_log); + _proxy = ProxyGen.CreateProxy(_log, _interceptor); + } + + [Fact] + public async Task ShouldLog4Entries() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal(10, results.Count); + Assert.Equal(4, _log.Count); + } + + [Fact] + public async Task ShouldAllowTimingPriorToInvocation() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal($"{MethodName}:StartingTiming", _log[0]); + } + + [Fact] + public async Task ShouldAllowTimingAfterInvocation() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal($"{MethodName}:CompletedTiming:{_interceptor.Stopwatch.Elapsed:g}", _log[3]); + } +} diff --git a/test/Castle.Core.AsyncInterceptor.Tests/Castle.Core.AsyncInterceptor.Tests.csproj b/test/Castle.Core.AsyncInterceptor.Tests/Castle.Core.AsyncInterceptor.Tests.csproj index 34c2745..b5c716f 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/Castle.Core.AsyncInterceptor.Tests.csproj +++ b/test/Castle.Core.AsyncInterceptor.Tests/Castle.Core.AsyncInterceptor.Tests.csproj @@ -1,7 +1,7 @@ - net472;netcoreapp3.1;net5.0;net6.0;net7.0 + netcoreapp3.1;net5.0;net6.0;net7.0 false $(NoWarn);SA0001 diff --git a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysCompletedAsync.cs b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysCompletedAsync.cs index 3609436..e58ebe6 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysCompletedAsync.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysCompletedAsync.cs @@ -57,4 +57,19 @@ public Task AsynchronousResultMethod() _log.Add(nameof(AsynchronousResultMethod) + ":End"); return Task.FromResult(Guid.NewGuid()); } + + public IAsyncEnumerable AsyncEnumerableMethod() + { + throw new NotImplementedException(); + } + + public IAsyncEnumerator AsyncEnumerableExceptionMethodNoReturnValues() + { + throw new NotImplementedException(); + } + + public IAsyncEnumerator AsyncEnumerableExceptionMethodReturnSomeValues() + { + throw new NotImplementedException(); + } } diff --git a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysIncompleteAsync.cs b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysIncompleteAsync.cs index 7c4b345..6ae5433 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysIncompleteAsync.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithAlwaysIncompleteAsync.cs @@ -58,4 +58,19 @@ public async Task AsynchronousResultMethod() _log.Add(nameof(AsynchronousResultMethod) + ":End"); return Guid.NewGuid(); } + + public IAsyncEnumerable AsyncEnumerableMethod() + { + throw new NotImplementedException(); + } + + public IAsyncEnumerator AsyncEnumerableExceptionMethodNoReturnValues() + { + throw new NotImplementedException(); + } + + public IAsyncEnumerator AsyncEnumerableExceptionMethodReturnSomeValues() + { + throw new NotImplementedException(); + } } diff --git a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithInterfaceToProxy.cs b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithInterfaceToProxy.cs index a95bedc..90aadfa 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithInterfaceToProxy.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/ClassWithInterfaceToProxy.cs @@ -73,4 +73,34 @@ public async Task AsynchronousResultExceptionMethod() await Task.Delay(10).ConfigureAwait(false); throw new InvalidOperationException(nameof(AsynchronousResultExceptionMethod) + ":Exception"); } + + public async IAsyncEnumerable AsyncEnumerableMethod() + { + _log.Add(nameof(AsyncEnumerableMethod) + ":Start"); + for (int i = 0; i < 10; i++) + { + await Task.Delay(10).ConfigureAwait(false); + yield return Guid.NewGuid(); + } + + _log.Add(nameof(AsyncEnumerableMethod) + ":End"); + } + + public IAsyncEnumerator AsyncEnumerableExceptionMethodNoReturnValues() + { + _log.Add(nameof(AsyncEnumerableExceptionMethodNoReturnValues) + ":Start"); + throw new InvalidOperationException(nameof(AsyncEnumerableExceptionMethodNoReturnValues) + ":Exception"); + } + + public async IAsyncEnumerator AsyncEnumerableExceptionMethodReturnSomeValues() + { + _log.Add(nameof(AsyncEnumerableExceptionMethodReturnSomeValues) + ":Start"); + for (int i = 0; i < 2; i++) + { + await Task.Delay(10).ConfigureAwait(false); + yield return Guid.NewGuid(); + } + + throw new InvalidOperationException(nameof(AsyncEnumerableExceptionMethodReturnSomeValues) + ":Exception"); + } } diff --git a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/IInterfaceToProxy.cs b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/IInterfaceToProxy.cs index f09c524..9bc7636 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/IInterfaceToProxy.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/IInterfaceToProxy.cs @@ -22,4 +22,10 @@ public interface IInterfaceToProxy Task AsynchronousResultMethod(); Task AsynchronousResultExceptionMethod(); + + IAsyncEnumerable AsyncEnumerableMethod(); + + IAsyncEnumerator AsyncEnumerableExceptionMethodNoReturnValues(); + + IAsyncEnumerator AsyncEnumerableExceptionMethodReturnSomeValues(); } diff --git a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/TestAsyncInterceptor.cs b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/TestAsyncInterceptor.cs index f6f7041..8b6357b 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/TestAsyncInterceptor.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/InterfaceProxies/TestAsyncInterceptor.cs @@ -29,6 +29,14 @@ public void InterceptAsynchronous(IInvocation invocation) invocation.ReturnValue = LogInterceptAsynchronous(invocation); } + public void InterceptAsyncEnumerable(IInvocation invocation) + { + LogInterceptStart(invocation); + invocation.Proceed(); + var innerEnumerable = (IAsyncEnumerable)invocation.ReturnValue; + invocation.ReturnValue = LogInterceptAsyncEnumerable(invocation, innerEnumerable); + } + private async Task LogInterceptAsynchronous(IInvocation invocation) { LogInterceptStart(invocation); @@ -48,6 +56,16 @@ private async Task LogInterceptAsynchronous(IInvocation invoca return result; } + private async IAsyncEnumerable LogInterceptAsyncEnumerable(IInvocation invocation, IAsyncEnumerable innerEnumerable) + { + await foreach (TResult result in innerEnumerable) + { + yield return result; + } + + LogInterceptEnd(invocation); + } + private void LogInterceptStart(IInvocation invocation) { _log.Add($"{invocation.Method.Name}:InterceptStart"); diff --git a/test/Castle.Core.AsyncInterceptor.Tests/ProcessingAsyncInterceptorShould.cs b/test/Castle.Core.AsyncInterceptor.Tests/ProcessingAsyncInterceptorShould.cs index 9fda695..c1a6993 100644 --- a/test/Castle.Core.AsyncInterceptor.Tests/ProcessingAsyncInterceptorShould.cs +++ b/test/Castle.Core.AsyncInterceptor.Tests/ProcessingAsyncInterceptorShould.cs @@ -192,3 +192,62 @@ public async Task ShouldAllowProcessingAfterInvocation() Assert.Equal($"{MethodName}:CompletedInvocation:{_interceptor.RandomValue}", _log[3]); } } + +public class WhenProcessingAsyncEnumerableMethods +{ + private const string MethodName = nameof(IInterfaceToProxy.AsyncEnumerableMethod); + private readonly ListLogger _log; + private readonly TestProcessingAsyncInterceptor _interceptor; + private readonly IInterfaceToProxy _proxy; + + public WhenProcessingAsyncEnumerableMethods(ITestOutputHelper output) + { + string randomValue = "randomValue_" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + _log = new ListLogger(output); + _interceptor = new TestProcessingAsyncInterceptor(_log, randomValue); + _proxy = ProxyGen.CreateProxy(_log, _interceptor); + } + + [Fact] + public async Task ShouldLog4Entries() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal(10, results.Count); + Assert.Equal(4, _log.Count); + } + + [Fact] + public async Task ShouldAllowProcessingPriorToInvocation() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal($"{MethodName}:StartingInvocation:{_interceptor.RandomValue}", _log[0]); + } + + [Fact] + public async Task ShouldAllowProcessingAfterInvocation() + { + // Act + List results = new(); + await foreach (Guid result in _proxy.AsyncEnumerableMethod()) + { + results.Add(result); + } + + // Assert + Assert.Equal($"{MethodName}:CompletedInvocation:{_interceptor.RandomValue}", _log[3]); + } +} From 4a9bfd9463ff48b55215ce4dda91bcf55ed63353 Mon Sep 17 00:00:00 2001 From: Arnaud Debaene Date: Wed, 4 Jan 2023 13:18:16 +0100 Subject: [PATCH 2/2] corrected build on BitBucket (error CA2208) --- .../AsyncDeterminationInterceptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs b/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs index 4841f27..5303904 100644 --- a/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs +++ b/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs @@ -116,7 +116,7 @@ private static GenericAsyncHandler CreateHandler(Type returnType) return (GenericAsyncHandler)method.CreateDelegate(typeof(GenericAsyncHandler)); } - throw new ArgumentException(nameof(returnType)); + throw new ArgumentException("Only Task, Task<> or IAsyncEnumerable<> return types are supported", nameof(returnType)); } ///