From 48a1ce6ea4c2ba0a8f9e6b2c6d65bee76473e4c9 Mon Sep 17 00:00:00 2001 From: moattarwork Date: Fri, 16 Nov 2018 13:11:52 +0000 Subject: [PATCH 1/2] #6 #3 #4 Included async interception and fix the bug with multiple interception --- src/Foil.Logging/InvocationExtensions.cs | 39 ++++ src/Foil.Sample/Foil.Sample.csproj | 14 +- src/Foil.Sample/Program.cs | 12 +- src/Foil.Sample/Services/ISampleService.cs | 2 +- src/Foil.Sample/Services/SampleService.cs | 4 +- src/Foil.UnitTests/Foil.UnitTests.csproj | 16 +- src/Foil.sln | 6 + src/Foil/AsyncInterceptor.cs | 173 ++++++++++++++++++ ... => ConventionBasedProxyGenerationHook.cs} | 5 +- src/Foil/Foil.csproj | 10 +- src/Foil/Interceptions/InterceptionOptions.cs | 2 +- src/Foil/ServiceCollectionExtensions.cs | 96 +++++++--- 12 files changed, 329 insertions(+), 50 deletions(-) create mode 100644 src/Foil.Logging/InvocationExtensions.cs create mode 100644 src/Foil/AsyncInterceptor.cs rename src/Foil/Conventions/{ConvensionBasedProxyGenerationHook.cs => ConventionBasedProxyGenerationHook.cs} (80%) diff --git a/src/Foil.Logging/InvocationExtensions.cs b/src/Foil.Logging/InvocationExtensions.cs new file mode 100644 index 0000000..93fbcb1 --- /dev/null +++ b/src/Foil.Logging/InvocationExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Castle.DynamicProxy; +using Newtonsoft.Json; + +namespace Foil.Logging +{ + public static class InvocationExtensions + { + public static string FormatArguments(this IInvocation invocation) + { + if (invocation == null) throw new ArgumentNullException(nameof(invocation)); + + var arguments = invocation.Arguments; + if (!arguments.Any()) + return string.Empty; + + + var serializedArguments = arguments + .Select((a, index) => $"Arg[{index}]: {a.ToJsonOrDefault()}") + .ToList(); + return string.Join(Environment.NewLine, serializedArguments); + + } + + private static string ToJsonOrDefault(this object obj) + { + if (obj == null) throw new ArgumentNullException(nameof(obj)); + try + { + return JsonConvert.SerializeObject(obj); + } + catch + { + return "Error in serializing argument to json"; + } + } + } +} \ No newline at end of file diff --git a/src/Foil.Sample/Foil.Sample.csproj b/src/Foil.Sample/Foil.Sample.csproj index 0a4734a..7897437 100755 --- a/src/Foil.Sample/Foil.Sample.csproj +++ b/src/Foil.Sample/Foil.Sample.csproj @@ -1,14 +1,18 @@  Exe - netcoreapp1.1 + netcoreapp1.1;netcoreapp2.0 - - - + + + + + + - + + \ No newline at end of file diff --git a/src/Foil.Sample/Program.cs b/src/Foil.Sample/Program.cs index 6a8a875..d8005ce 100755 --- a/src/Foil.Sample/Program.cs +++ b/src/Foil.Sample/Program.cs @@ -1,6 +1,8 @@ -using Foil.Sample.Interceptors; +using Foil.Logging; +using Foil.Sample.Interceptors; using Foil.Sample.Services; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Foil.Sample { @@ -10,12 +12,16 @@ static void Main(string[] args) { var services = new ServiceCollection(); - services.AddTransientWithInterception(m => m.InterceptBy()); + services.AddLogging(); + services.AddTransientWithInterception(m => m.InterceptBy().ThenBy()); var provider = services.BuildServiceProvider(); + var loggingFactory = provider.GetRequiredService(); + loggingFactory.AddConsole(LogLevel.Trace); var service = provider.GetRequiredService(); - service.Call(); + + service.Call("Dear User"); } } } \ No newline at end of file diff --git a/src/Foil.Sample/Services/ISampleService.cs b/src/Foil.Sample/Services/ISampleService.cs index e104176..d1ee3b7 100644 --- a/src/Foil.Sample/Services/ISampleService.cs +++ b/src/Foil.Sample/Services/ISampleService.cs @@ -2,6 +2,6 @@ { public interface ISampleService { - void Call(); + void Call(string sample); } } \ No newline at end of file diff --git a/src/Foil.Sample/Services/SampleService.cs b/src/Foil.Sample/Services/SampleService.cs index c59ed3d..36af21e 100644 --- a/src/Foil.Sample/Services/SampleService.cs +++ b/src/Foil.Sample/Services/SampleService.cs @@ -4,9 +4,9 @@ namespace Foil.Sample.Services { public class SampleService : ISampleService { - public virtual void Call() + public virtual void Call(string sample) { - Console.WriteLine("Hello Sample"); + Console.WriteLine($"Hello {sample}"); } } } \ No newline at end of file diff --git a/src/Foil.UnitTests/Foil.UnitTests.csproj b/src/Foil.UnitTests/Foil.UnitTests.csproj index ec28b2d..297e84f 100755 --- a/src/Foil.UnitTests/Foil.UnitTests.csproj +++ b/src/Foil.UnitTests/Foil.UnitTests.csproj @@ -1,16 +1,16 @@  - netcoreapp1.1 + netcoreapp1.1;netcoreapp2.0 - - - - - - + + + + + + - + \ No newline at end of file diff --git a/src/Foil.sln b/src/Foil.sln index 34c1608..9e12c56 100644 --- a/src/Foil.sln +++ b/src/Foil.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foil.UnitTests", "Foil.Unit EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foil.Sample", "Foil.Sample\Foil.Sample.csproj", "{3AD6C03C-F18A-4F95-9575-FC4B32D83F40}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foil.Logging", "Foil.Logging\Foil.Logging.csproj", "{22710EAF-09BE-495D-A7EF-23F262ACF80A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {3AD6C03C-F18A-4F95-9575-FC4B32D83F40}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AD6C03C-F18A-4F95-9575-FC4B32D83F40}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AD6C03C-F18A-4F95-9575-FC4B32D83F40}.Release|Any CPU.Build.0 = Release|Any CPU + {22710EAF-09BE-495D-A7EF-23F262ACF80A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22710EAF-09BE-495D-A7EF-23F262ACF80A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22710EAF-09BE-495D-A7EF-23F262ACF80A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22710EAF-09BE-495D-A7EF-23F262ACF80A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Foil/AsyncInterceptor.cs b/src/Foil/AsyncInterceptor.cs new file mode 100644 index 0000000..d0ace65 --- /dev/null +++ b/src/Foil/AsyncInterceptor.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; +using System.Threading.Tasks; +using Castle.DynamicProxy; + +namespace Foil +{ + public abstract class AsyncInterceptor : IInterceptor + { + private static readonly MethodInfo InterceptSynchronousMethodInfo = + typeof(AsyncInterceptor) + .GetMethod(nameof(InterceptSynchronousResult), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly ConcurrentDictionary GenericSynchronousHandlers = + new ConcurrentDictionary + { + [typeof(void)] = InterceptSynchronousVoid + }; + + private static readonly MethodInfo HandleAsyncMethodInfo = + typeof(AsyncInterceptor) + .GetMethod(nameof(HandleAsyncWithResult), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly ConcurrentDictionary GenericAsyncHandlers = + new ConcurrentDictionary(); + + public void Intercept(IInvocation invocation) + { + var methodType = GetMethodType(invocation.Method.ReturnType); + + switch (methodType) + { + case MethodType.AsyncAction: + InterceptAsynchronous(invocation); + return; + case MethodType.AsyncFunction: + GetHandler(invocation.Method.ReturnType).Invoke(invocation, this); + return; + default: + InterceptSynchronous(invocation); + return; + } + } + + private static MethodType GetMethodType(Type returnType) + { + if (returnType == typeof(void) || !typeof(Task).IsAssignableFrom(returnType)) + return MethodType.Synchronous; + + return returnType.GetTypeInfo().IsGenericType ? MethodType.AsyncFunction : MethodType.AsyncAction; + } + + private static GenericAsyncHandler GetHandler(Type returnType) + { + var handler = GenericAsyncHandlers.GetOrAdd(returnType, CreateHandlerAsync); + return handler; + } + + private static void HandleAsyncWithResult(IInvocation invocation, + AsyncInterceptor asyncInterceptor) + { + asyncInterceptor.InterceptAsynchronous(invocation); + } + + public void InterceptSynchronous(IInvocation invocation) + { + var returnType = invocation.Method.ReturnType; + var handler = GenericSynchronousHandlers.GetOrAdd(returnType, CreateHandlerForSync); + handler(this, invocation); + } + + private static GenericSynchronousHandler CreateHandlerForSync(Type returnType) + { + var method = InterceptSynchronousMethodInfo.MakeGenericMethod(returnType); + return (GenericSynchronousHandler) method.CreateDelegate(typeof(GenericSynchronousHandler)); + } + + private static GenericAsyncHandler CreateHandlerAsync(Type returnType) + { + var taskReturnType = returnType.GetGenericArguments()[0]; + var method = HandleAsyncMethodInfo.MakeGenericMethod(taskReturnType); + return (GenericAsyncHandler) method.CreateDelegate(typeof(GenericAsyncHandler)); + } + + public void InterceptAsynchronous(IInvocation invocation) + { + invocation.ReturnValue = InterceptAsync(invocation, ProceedAsynchronous); + } + + public void InterceptAsynchronous(IInvocation invocation) + { + invocation.ReturnValue = InterceptAsync(invocation, ProceedAsynchronous); + } + + protected abstract Task InterceptAsync(IInvocation invocation, Func proceed); + + protected abstract Task InterceptAsync( + IInvocation invocation, + Func> proceed); + + private static void InterceptSynchronousVoid(AsyncInterceptor me, IInvocation invocation) + { + var task = me.InterceptAsync(invocation, ProceedSynchronous); + + if (!task.IsCompleted) Task.Run(() => task).Wait(); + + if (task.IsFaulted) throw task.Exception.InnerException; + } + + private static void InterceptSynchronousResult(AsyncInterceptor me, IInvocation invocation) + { + Task task = me.InterceptAsync(invocation, ProceedSynchronous); + + if (!task.IsCompleted) Task.Run(() => task).Wait(); + + if (task.IsFaulted) throw task.Exception.InnerException; + } + + private static Task ProceedSynchronous(IInvocation invocation) + { + try + { + invocation.Proceed(); + return Task.CompletedTask; + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + private static Task ProceedSynchronous(IInvocation invocation) + { + try + { + invocation.Proceed(); + return Task.FromResult((TResult) invocation.ReturnValue); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + private static async Task ProceedAsynchronous(IInvocation invocation) + { + invocation.Proceed(); + var originalReturnValue = (Task) invocation.ReturnValue; + await originalReturnValue.ConfigureAwait(false); + } + + private static async Task ProceedAsynchronous(IInvocation invocation) + { + invocation.Proceed(); + var originalReturnValue = (Task) invocation.ReturnValue; + + var result = await originalReturnValue.ConfigureAwait(false); + return result; + } + + private delegate void GenericSynchronousHandler(AsyncInterceptor me, IInvocation invocation); + + private delegate void GenericAsyncHandler(IInvocation invocation, AsyncInterceptor asyncInterceptor); + + private enum MethodType + { + Synchronous, + AsyncAction, + AsyncFunction + } + } +} \ No newline at end of file diff --git a/src/Foil/Conventions/ConvensionBasedProxyGenerationHook.cs b/src/Foil/Conventions/ConventionBasedProxyGenerationHook.cs similarity index 80% rename from src/Foil/Conventions/ConvensionBasedProxyGenerationHook.cs rename to src/Foil/Conventions/ConventionBasedProxyGenerationHook.cs index 820a5f1..ad71aea 100644 --- a/src/Foil/Conventions/ConvensionBasedProxyGenerationHook.cs +++ b/src/Foil/Conventions/ConventionBasedProxyGenerationHook.cs @@ -2,15 +2,14 @@ using System.Reflection; using Castle.DynamicProxy; using Foil.Interceptions; -using Microsoft.Extensions.Logging; namespace Foil.Conventions { - public class ConvensionBasedProxyGenerationHook : IProxyGenerationHook + public class ConventionBasedProxyGenerationHook : IProxyGenerationHook { private readonly IMethodConvention _convention; - public ConvensionBasedProxyGenerationHook(IMethodConvention convention) + public ConventionBasedProxyGenerationHook(IMethodConvention convention) { _convention = convention ?? throw new ArgumentNullException(nameof(convention)); } diff --git a/src/Foil/Foil.csproj b/src/Foil/Foil.csproj index 09da4f4..a7f4549 100644 --- a/src/Foil/Foil.csproj +++ b/src/Foil/Foil.csproj @@ -1,13 +1,13 @@  Library - netcoreapp1.1 1.0.0 + netstandard1.6;netstandard2.0 - - - - + + + + \ No newline at end of file diff --git a/src/Foil/Interceptions/InterceptionOptions.cs b/src/Foil/Interceptions/InterceptionOptions.cs index 560f4c1..ebf92b8 100644 --- a/src/Foil/Interceptions/InterceptionOptions.cs +++ b/src/Foil/Interceptions/InterceptionOptions.cs @@ -17,7 +17,7 @@ public class InterceptionOptions : IInterceptBy, IThenInterceptBy public IThenInterceptBy ThenBy() where TInterceptor : IInterceptor { - if (_interceptors.ContainsKey(typeof(TInterceptor))) + if (!_interceptors.ContainsKey(typeof(TInterceptor))) _interceptors.Add(typeof(TInterceptor), typeof(TInterceptor)); return this; diff --git a/src/Foil/ServiceCollectionExtensions.cs b/src/Foil/ServiceCollectionExtensions.cs index 914735a..2169ebb 100644 --- a/src/Foil/ServiceCollectionExtensions.cs +++ b/src/Foil/ServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Castle.DynamicProxy; -using Castle.DynamicProxy.Generators; using Foil.Conventions; using Foil.Interceptions; using Microsoft.Extensions.DependencyInjection; @@ -12,45 +11,97 @@ namespace Foil public static class ServiceCollectionExtensions { public static IServiceCollection AddTransientWithInterception( - this IServiceCollection services, Action action) + this IServiceCollection services, + Func serviceFactory, + Action configurator) + where T : class where TImplementation : class, T + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + if (serviceFactory == null) + throw new ArgumentNullException(nameof(serviceFactory)); + + return services.Add( + lifetime => new ServiceDescriptor(typeof(TImplementation), serviceFactory, lifetime), + configurator, ServiceLifetime.Transient); + } + + public static IServiceCollection AddScopedWithInterception(this IServiceCollection services, + Func serviceFactory, + Action configurator) + where T : class where TImplementation : class, T + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + if (serviceFactory == null) + throw new ArgumentNullException(nameof(serviceFactory)); + + return services.Add( + lifetime => new ServiceDescriptor(typeof(TImplementation), serviceFactory, lifetime), + configurator, ServiceLifetime.Scoped); + } + + public static IServiceCollection AddSingletonWithInterception( + this IServiceCollection services, + Func serviceFactory, + Action configurator) + where T : class where TImplementation : class, T + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + if (serviceFactory == null) + throw new ArgumentNullException(nameof(serviceFactory)); + + return services.Add( + lifetime => new ServiceDescriptor(typeof(TImplementation), serviceFactory, lifetime), + configurator, ServiceLifetime.Singleton); + } + + public static IServiceCollection AddTransientWithInterception( + this IServiceCollection services, Action configurator) where T : class where TImplementation : class, T { - return AddWithInterception(services, action, ServiceLifetime.Transient); + return Add(services, + lifetime => ServiceDescriptor.Describe(typeof(TImplementation), typeof(TImplementation), lifetime), + configurator, ServiceLifetime.Transient); } public static IServiceCollection AddScopedWithInterception( - this IServiceCollection services, Action action) + this IServiceCollection services, Action configurator) where T : class where TImplementation : class, T { - return AddWithInterception(services, action, ServiceLifetime.Scoped); + return Add(services, + lifetime => ServiceDescriptor.Describe(typeof(TImplementation), typeof(TImplementation), lifetime), + configurator, ServiceLifetime.Scoped); } public static IServiceCollection AddSingletonWithInterception( - this IServiceCollection services, Action action) + this IServiceCollection services, Action configurator) where T : class where TImplementation : class, T { - return AddWithInterception(services, action, ServiceLifetime.Singleton); + if (services == null) throw new ArgumentNullException(nameof(services)); + return Add(services, + lifetime => ServiceDescriptor.Describe(typeof(TImplementation), typeof(TImplementation), lifetime), + configurator, ServiceLifetime.Singleton); } - private static IServiceCollection AddWithInterception(this IServiceCollection services, - Action action, ServiceLifetime lifetime) - where T : class - where TImplementation : class, T + private static IServiceCollection Add(this IServiceCollection services, + Func descriptorFactory, Action configurator, + ServiceLifetime lifetime) where TService : class where TImplementation : class, TService { - if (services == null) throw new ArgumentNullException(nameof(services)); - if (!Enum.IsDefined(typeof(ServiceLifetime), lifetime)) - throw new ArgumentOutOfRangeException(nameof(lifetime), - "Value should be defined in the ServiceLifetime enum."); + if (services == null) + throw new ArgumentNullException(nameof(services)); + if (configurator == null) throw new ArgumentNullException(nameof(configurator)); + var interceptionOptions = new InterceptionOptions(); - action?.Invoke(interceptionOptions); + configurator.Invoke(interceptionOptions); interceptionOptions.Interceptors.ForEach(services.TryAddTransient); - - services.TryAdd(ServiceDescriptor.Describe(typeof(TImplementation), typeof(TImplementation), lifetime)); + services.TryAdd(descriptorFactory(lifetime)); services.AddTransient(sp => { @@ -58,14 +109,15 @@ private static IServiceCollection AddWithInterception(this I .Select(sp.GetRequiredService) .Cast() .ToArray(); - + var implementation = sp.GetRequiredService(); var proxyFactory = new ProxyGenerator(); - var proxyGenerationHook = new ConvensionBasedProxyGenerationHook(interceptionOptions.Convention); + var proxyGenerationHook = new ConventionBasedProxyGenerationHook(interceptionOptions.Convention); var proxyGenerationOptions = new ProxyGenerationOptions(proxyGenerationHook); - - return proxyFactory.CreateInterfaceProxyWithTarget(implementation, proxyGenerationOptions, interceptorInstances); + + return proxyFactory.CreateInterfaceProxyWithTarget(implementation, proxyGenerationOptions, + interceptorInstances); }); return services; From 58405fd14c7316dbf813dfc13e9499f0ab1d4495 Mon Sep 17 00:00:00 2001 From: moattarwork Date: Fri, 16 Nov 2018 13:15:40 +0000 Subject: [PATCH 2/2] Included async interception and fix the bug with multiple interception --- src/Foil.Logging/Foil.Logging.csproj | 16 +++++++++++++ src/Foil.Logging/LoggingInterceptor.cs | 33 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/Foil.Logging/Foil.Logging.csproj create mode 100644 src/Foil.Logging/LoggingInterceptor.cs diff --git a/src/Foil.Logging/Foil.Logging.csproj b/src/Foil.Logging/Foil.Logging.csproj new file mode 100644 index 0000000..bcc8d54 --- /dev/null +++ b/src/Foil.Logging/Foil.Logging.csproj @@ -0,0 +1,16 @@ + + + + Library + 1.0.0 + netstandard1.6;netstandard2.0 + + + + + + + + + + diff --git a/src/Foil.Logging/LoggingInterceptor.cs b/src/Foil.Logging/LoggingInterceptor.cs new file mode 100644 index 0000000..3dd63bc --- /dev/null +++ b/src/Foil.Logging/LoggingInterceptor.cs @@ -0,0 +1,33 @@ +using System; +using Castle.DynamicProxy; +using Microsoft.Extensions.Logging; + +namespace Foil.Logging +{ + public sealed class LoggingInterceptor : IInterceptor + { + private readonly ILogger _logger; + + public LoggingInterceptor(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void Intercept(IInvocation invocation) + { + _logger.LogInformation($"Executing Method: {invocation.Method.Name}"); + + LogInvocationInfo(invocation); + + invocation.Proceed(); + + _logger.LogInformation($"Executed Method: {invocation.Method.Name}"); + } + + private void LogInvocationInfo(IInvocation invocation) + { + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace(invocation.FormatArguments()); + } + } +} \ No newline at end of file