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

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

2017 年 12 月 5 日

MIT

4分钟阅读

viewsIcon

33672

downloadIcon

266

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

本文的源代码和使用 RealProxy 的示例(包括单元测试)可以在 GitHub 上找到,或从上面的链接下载。

引言

面向切面编程 (AOP) 是一种非常强大的方法,可以避免样板代码并实现更好的模块化。其主要思想是在不修改现有代码的情况下向其添加行为(通知)。AOP 提供了一种将切面注入代码的方式。切面应该是通用的,因此可以应用于任何对象,并且对象不应该了解通知的任何信息。AOP 允许分离 横切关注点,并使遵循 单一职责原则SOLID 原则之一)变得更加容易。日志记录、安全、事务和异常处理是使用 AOP 的最常见示例。如果您不熟悉这种编程技术,可以阅读 这篇这篇。这可能非常有益,因为本文主要关注如何在 C# 中使用 AOP,而不是 AOP 是什么。如果您仍然不理解它的全部含义,请不要害怕。在查看了几个示例后,它会变得更容易理解。

在 Java 中,AOP 在 AspectJSpring 框架中实现。在 .NET 中,有 PostSharp(非免费)、NConcern 以及一些其他框架(不太流行且不易使用)可以实现几乎相同的功能。

也可以使用 RealProxy 类来实现 AOP。您可以在此处找到一些如何执行此操作的示例。

示例 1面向切面编程:使用 RealProxy 类进行面向切面编程

本文还包含大量关于 AOP 是什么、装饰器设计模式 如何工作以及使用 AOP 实现日志记录和身份验证的示例。

示例 2MSDN

不幸的是,这些示例存在一些显著的缺点。示例 1 不支持 out 参数。示例 2 有一个限制:被装饰的类应继承自 MarshalByRefObject(如果它不是您设计的类,这可能会有问题)。此外,两个示例都不如预期地支持异步函数。几个月前,我修改了第一个示例以支持 Task 结果和输出参数,并写了一篇关于它的文章。

示例 3使用 RealProxy 在 C# 中进行面向切面编程

不幸的是,.NET Core 没有 RealProxy 类。取而代之的是 DispatchProxy 类。使用 DispatchProxy 类与使用 RealProxy 类的使用方式略有不同。鉴于关于使用 DispatchProxy 的示例不多,本文也可以被视为其中一个示例。

让我们使用 DispatchProxy 类来实现日志记录。

解决方案

用于记录异常的扩展(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)

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

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        if (targetMethod != null)
        {
            try
            {
                try
                {
                    LogBefore(targetMethod, args);
                }
                catch (Exception ex)
                {
                    //Do not stop method execution if exception
                    LogException(ex);
                }

                var result = targetMethod.Invoke(_decorated, args);
                var resultTask = result as Task;

                if (resultTask != null)
                {
                    resultTask.ContinueWith(task =>
                        {
                            if (task.Exception != null)
                            {
                                LogException(task.Exception.InnerException ?? task.Exception, 
                                             targetMethod);
                            }
                            else
                            {
                                object taskResult = null;
                                if (task.GetType().GetTypeInfo().IsGenericType &&
                                    task.GetType().GetGenericTypeDefinition() == typeof(Task<>))
                                {
                                    var property = task.GetType().GetTypeInfo().GetProperties()
                                        .FirstOrDefault(p => p.Name == "Result");
                                    if (property != null)
                                    {
                                        taskResult = property.GetValue(task);
                                    }
                                }

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

                return result;
            }
            catch (Exception ex)
            {
                if (ex is TargetInvocationException)
                {
                    LogException(ex.InnerException ?? ex, targetMethod);
                    throw ex.InnerException ?? ex;
                }
            }
        }

        throw new ArgumentException(nameof(targetMethod));
    }

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

        return (T)proxy;
    }

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

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

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

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

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

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

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

    private void LogAfter(MethodInfo methodInfo, object[] args, object result)
    {
        var afterMessage = new StringBuilder();
        afterMessage.AppendLine($"Class {_decorated.GetType().FullName}");
        afterMessage.AppendLine($"Method {methodInfo.Name} 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(MethodInfo methodInfo, object[] args)
    {
        var beforeMessage = new StringBuilder();
        beforeMessage.AppendLine($"Class {_decorated.GetType().FullName}");
        beforeMessage.AppendLine($"Method {methodInfo.Name} 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 = args[i];
                beforeMessage.AppendLine($"{parameter.Name}:{GetStringValue(arg)}");
            }
        }

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

我们假设有一个接口和一个类。

public interface IMyClass
{
    int MyMethod(string param);
}

public class MyClass: IMyClass
{
    public int MyMethod(string param)
    {
        return param.Length;
    }
}

要用 LoggingAdvice 装饰 MyClass,我们应该这样做:

var decorated = LoggingAdvice<IMyClass>.Create(
                new MyClass(),
                s => Debug.WriteLine("Info:" + s),
                s => Debug.WriteLine("Error:" + s),
                o => o?.ToString());

要理解它的工作原理,我们调用 decorated 实例的 MyMesthod

var length = decorated.MyMethod("Hello world!");

这行代码的作用是:

  1. decorated.MyMethod("Hello world!") 调用 LoggingAdviceInvoke 方法,其中 targetMethod 等于 MyMethodargs 等于一个包含一个元素的数组,该元素为 "Hello world!"
  2. LoggingAdvice 类的 Invoke 方法记录 MyMethod 方法名和输入参数(LogBefore)。
  3. LoggingAdvice 类的 Invoke 方法调用 MyClassMyMethod 方法。
  4. 如果方法调用成功,则记录输出参数和结果(LogAfter),并且 Invoke 方法返回结果。
  5. 如果方法调用引发异常,则记录异常(LogException),并且 Invoke 引发相同的异常。
  6. Invoke 方法执行的结果(结果或异常)将作为调用 decorated 对象 MyMethod 的结果返回。

示例

假设我们将实现一个计算器,用于整数加减法。

public interface ICalculator
{
    int Add(int a, int b);
    int Subtract(int a, int b);
}

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。您不明白发生了什么,决定添加日志记录。

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 是一个接口),但仍然耦合。每次更改 ILogger 接口时,都会影响 Calculator
  2. 代码变得更复杂。
  3. 这违反了单一职责原则。Add 函数不仅仅是相加。它还会记录输入值、相加值并记录结果。Subtract 同理。

本文中的代码让您无需触碰 Calculator 类。

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

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.