65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 RealProxy 的 C# 面向方面编程

2017年9月6日

MIT

3分钟阅读

viewsIcon

34468

downloadIcon

81

使用 RealProxy 类实现面向方面编程日志记录的示例

引言

面向切面编程 (AOP) 是一种非常强大的方法,可以避免样板代码并实现更好的模块化。其主要思想是在不修改现有代码本身的情况下,向现有代码添加行为(通知)。在 Java 中,这个思想在 AspectJSpring 框架中得到了实现。在 .NET 中,有 PostSharp(非免费)、NConcern 和一些其他框架(不太流行且不易使用)可以实现几乎相同的功能。

还可以使用 RealProxy 类来实现 AOP。您可以在此处找到一些关于如何实现它的示例

 

示例 1面向切面编程:使用 RealProxy 类实现面向切面编程

示例 2MSDN

不幸的是,这些示例存在一些明显的缺点。示例 1 不支持 out 参数。示例 2 有一个限制。被装饰的类应该继承自 MarshalByRefObject(如果不是您自己的类,这可能会有问题)。此外,这两个示例都不支持异步函数。好吧,严格来说是支持的。但我期望“after”代码在任务完成后执行,如果“after”代码尝试获取 Task 对象的结果(例如,在结果序列化期间),它会将异步代码变成同步的(不好 ☹)。

我尝试修复第一个示例。

在继续阅读之前

本文介绍如何修复 **Bruno Sonnino**(示例 1)提供的解决方案中的一些问题。该文章对代码的工作方式以及它解决的问题类型进行了很好的解释。请先阅读 面向切面编程:使用 RealProxy 类实现面向切面编程

替代解决方案

还可以使用 DispatchProxy 来实现相同的功能。您可以在我的文章 使用 DispatchProxy 在 C# 中实现面向切面编程 中找到一个实现该功能的示例。

源代码

本文的代码以及用于 DispatchProxy 的示例(以及两者的单元测试)可以在 GitHub 上找到。

解决方案

此解决方案是日志记录实现的示例。代码可以在 此处 找到。

与原始代码的区别

  1. 添加了扩展方法 GetDescription 来记录异常数据 (Extensions.cs)。
  2. DynamicProxy 类已重命名为 LoggingAdvice
  3. 构造函数被设为 privateStatic 函数 Create 创建类实例并返回 TransparentProxy (LoggingAdvice.cs - 行 35-41)。这使得不可能创建 LoggingAdvice 类的实例,因为只有由该类创建的代理将被使用。
  4. LoggingAdvice 接收用于记录函数调用和错误的操作,以及用于序列化复杂类型值作为参数的函数 (LoggingAdvice.cs - 行 19-20)。
  5. 添加了 TaskScheduler 作为可选参数,以支持使用不同的任务调度程序进行任务结果日志记录。默认将使用 TaskScheduler.FromCurrentSynchronizationContext() (LoggingAdvice.cs - 行 36)。
  6. 添加了函数 LogExceptionLogBeforeLogAfter 来记录相应的数据 (LoggingAdvice.cs - 行 150-209)。
  7. 添加了 Try/catch 块来处理 logInfo 函数抛出异常的情况 (LoggingAdvice.cs - 行 53-61, 99-107)。
  8. 如果函数的返回值是 Task,执行结果将在任务完成后记录 (LoggingAdvice.cs - 行 69-96)。
  • 添加了输出参数支持 (LoggingAdvice.cs - 行 63, 110-111)。

记录异常的扩展 (Extensions.cs)

using System;
using System.Text;

namespace AOP
{
    public static class Extensions
    {
        public static string GetDescription(this Exception e)
        {
            var builder = new StringBuilder();
            AddException(builder, e);

            return builder.ToString();
        }

        private static void AddException(StringBuilder builder, Exception e)
        {
            builder.AppendLine($"Message: {e.Message}");
            builder.AppendLine($"Stack Trace: {e.StackTrace}");
            if (e.InnerException != null)
            {
                builder.AppendLine("Inner Exception");
                AddException(builder, e.InnerException);
            }
        }
    }
}

日志通知 (LoggingAdvice.cs)

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Text;
using System.Threading.Tasks;

namespace AOP
{
    public class LoggingAdvice<T> : RealProxy
    {
        private readonly T _decorated;
        private readonly Action<string> _logInfo;
        private readonly Action<string> _logError;
        private readonly Func<object, string> _serializeFunction;
        private readonly TaskScheduler _loggingScheduler;

        private LoggingAdvice(T decorated, Action<string> logInfo, Action<string> logError,
            Func<object, string> serializeFunction, TaskScheduler loggingScheduler)
            : base(typeof(T))
        {
            if (decorated == null)
            {
                throw new ArgumentNullException(nameof(decorated));
            }

            _decorated = decorated;
            _logInfo = logInfo;
            _logError = logError;
            _serializeFunction = serializeFunction;
            _loggingScheduler = loggingScheduler ?? TaskScheduler.FromCurrentSynchronizationContext();
        }

        public static T Create(T decorated, Action<string> logInfo, Action<string> logError,
            Func<object, string> serializeFunction, TaskScheduler loggingScheduler = null)
        {
            var advice = new LoggingAdvice<T>
                         (decorated, logInfo, logError, serializeFunction, loggingScheduler);

            return (T)advice.GetTransparentProxy();
        }

        public override IMessage Invoke(IMessage msg)
        {
            var methodCall = msg as IMethodCallMessage;
            if (methodCall != null)
            {
                var methodInfo = methodCall.MethodBase as MethodInfo;
                if (methodInfo != null)
                {
                    try
                    {
                        try
                        {
                            LogBefore(methodCall, methodInfo);
                        }
                        catch (Exception ex)
                        {
                            //Do not stop method execution if exception
                            LogException(ex);
                        }

                        var args = methodCall.Args;

                        var result = typeof(T).InvokeMember(
                            methodCall.MethodName,
                            BindingFlags.InvokeMethod | BindingFlags.Public | 
                            BindingFlags.Instance, null, _decorated, args);

                        if (result is Task)
                        {
                            ((Task)result).ContinueWith(task =>
                           {
                               if (task.Exception != null)
                               {
                                   LogException(task.Exception.InnerException ?? task.Exception, 
                                                methodCall);
                               }
                               else
                               {
                                   object taskResult = null;

                                   if (task.GetType().IsGenericType && 
                                      task.GetType().GetGenericTypeDefinition() == typeof(Task<>))
                                   {
                                       var property = task.GetType().GetProperties()
                                           .FirstOrDefault(p => p.Name == "Result");

                                       if (property != null)
                                       {
                                           taskResult = property.GetValue(task);
                                       }
                                   }

                                   LogAfter(methodCall, methodCall.Args, methodInfo, taskResult);
                               }
                           },
                           _loggingScheduler);
                        }
                        else
                        {
                            try
                            {
                                LogAfter(methodCall, args, methodInfo, result);
                            }
                            catch (Exception ex)
                            {
                                //Do not stop method execution if exception
                                LogException(ex);
                            }
                        }

                        return new ReturnMessage(result, args, args.Length,
                            methodCall.LogicalCallContext, methodCall);
                    }
                    catch (Exception ex)
                    {
                        if (ex is TargetInvocationException)
                        {
                            LogException(ex.InnerException ?? ex, methodCall);

                            return new ReturnMessage(ex.InnerException ?? ex, methodCall);
                        }
                    }
                }
            }

            throw new ArgumentException(nameof(msg));
        }

        private string GetStringValue(object obj)
        {
            if (obj == null)
            {
                return "null";
            }

            if (obj.GetType().IsPrimitive || obj.GetType().IsEnum || obj is string)
            {
                return obj.ToString();
            }

            try
            {
                return _serializeFunction?.Invoke(obj) ?? obj.ToString();
            }
            catch
            {
                return obj.ToString();
            }
        }

        private void LogException(Exception exception, IMethodCallMessage methodCall = null)
        {
            try
            {
                var errorMessage = new StringBuilder();
                errorMessage.AppendLine($"Class {_decorated.GetType().FullName}");
                errorMessage.AppendLine($"Method {methodCall?.MethodName} threw exception");
                errorMessage.AppendLine(exception.GetDescription());

                _logError?.Invoke(errorMessage.ToString());
            }
            catch (Exception)
            {
                // ignored
                //Method should return original exception
            }
        }

        private void LogAfter(IMethodCallMessage methodCall, object[] args, 
                                          MethodInfo methodInfo, object result)
        {
            var afterMessage = new StringBuilder();
            afterMessage.AppendLine($"Class {_decorated.GetType().FullName}");
            afterMessage.AppendLine($"Method {methodCall.MethodName} executed");
            afterMessage.AppendLine("Output:");
            afterMessage.AppendLine(GetStringValue(result));
            var parameters = methodInfo.GetParameters();
            if (parameters.Any())
            {
                afterMessage.AppendLine("Parameters:");
                for (var i = 0; i < parameters.Length; i++)
                {
                    var parameter = parameters[i];
                    var arg = args[i];
                    afterMessage.AppendLine($"{parameter.Name}:{GetStringValue(arg)}");
                }
            }

            _logInfo?.Invoke(afterMessage.ToString());
        }

        private void LogBefore(IMethodCallMessage methodCall, MethodInfo methodInfo)
        {
            var beforeMessage = new StringBuilder();
            beforeMessage.AppendLine($"Class {_decorated.GetType().FullName}");
            beforeMessage.AppendLine($"Method {methodCall.MethodName} executing");
            var parameters = methodInfo.GetParameters();
            if (parameters.Any())
            {
                beforeMessage.AppendLine("Parameters:");
                for (var i = 0; i < parameters.Length; i++)
                {
                    var parameter = parameters[i];
                    var arg = methodCall.Args[i];
                    beforeMessage.AppendLine($"{parameter.Name}:{GetStringValue(arg)}");
                }
            }

            _logInfo?.Invoke(beforeMessage.ToString());
        }
    }
}

如何使用

var decoratedInstance = LoggingAdvice<IInstanceInteface>.Create(
                instance,
                s => Console.WriteLine(“Info:” + s),
                s => Console.WriteLine(“Error:” + s),
                o => o?.ToString());

示例

假设我们要实现一个计算器,用于对整数进行加减运算。

namespace AOP.Example
{
    public interface ICalculator
    {
        int Add(int a, int b);
        int Subtract(int a, int b);
    }
}
namespace AOP.Example
{
    public class Calculator : ICalculator
    {
        public int Add(int a, int b)
        {
            return a + b;
        }

        public int Subtract(int a, int b)
        {
            return a - b;
        }
    }
}

这很简单。每个方法只有一个职责。

有一天,一些用户开始抱怨 Add(2, 2) 有时返回 5。您不明白发生了什么,决定添加日志记录。

namespace AOP.Example
{
    public class CalculatorWithoutAop: ICalculator
    {
        private readonly ILogger _logger;

        public CalculatorWithoutAop(ILogger logger)
        {
            _logger = logger;
        }

        public int Add(int a, int b)
        {
            _logger.Log($"Adding {a} + {b}");
            var result = a + b;
            _logger.Log($"Result is {result}");

            return result;
        }

        public int Subtract(int a, int b)
        {
            _logger.Log($"Subtracting {a} - {b}");
            var result = a - b;
            _logger.Log($"Result is {result}");

            return result;
        }

    }
}

此解决方案存在 3 个问题:

  1. Calculator 类与日志记录耦合。松耦合(因为 ILogger 是一个接口),但仍然耦合。每次更改此接口时,都会影响 Calculator
  2. 代码变得更复杂。
  3. 这违反了单一职责原则。Add 函数不仅仅是加法。它记录输入值、加法值并记录结果。Subtract 也是如此。

本文中的代码允许您完全不触碰 Calculator 类。

您只需要更改类的创建方式。

namespace AOP.Example
{
    public class CalculatorFactory
    {
        private readonly ILogger _logger;

        public CalculatorFactory(ILogger logger)
        {
            _logger = logger;
        }

        public ICalculator CreateCalculator()
        {
            return LoggingAdvice <ICalculator >.Create(
                new Calculator(),
                s => _logger.Log("Info:" + s),
                s => _logger.Log("Error:" + s),
                o => o?.ToString());
        }
    }
}

结论

此代码适用于我的情况。如果您有任何代码无效的示例,或者此代码可以改进的地方,请随时以任何方式与我联系。

就是这样——尽情享受吧!

© . All rights reserved.