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






4.96/5 (18投票s)
使用 DispatchProxy 类实现面向方面编程日志记录的示例
本文的源代码和使用 RealProxy
的示例(包括单元测试)可以在 GitHub 上找到,或从上面的链接下载。
引言
面向切面编程 (AOP) 是一种非常强大的方法,可以避免样板代码并实现更好的模块化。其主要思想是在不修改现有代码的情况下向其添加行为(通知)。AOP 提供了一种将切面注入代码的方式。切面应该是通用的,因此可以应用于任何对象,并且对象不应该了解通知的任何信息。AOP 允许分离 横切关注点,并使遵循 单一职责原则(SOLID 原则之一)变得更加容易。日志记录、安全、事务和异常处理是使用 AOP 的最常见示例。如果您不熟悉这种编程技术,可以阅读 这篇 或 这篇。这可能非常有益,因为本文主要关注如何在 C# 中使用 AOP,而不是 AOP 是什么。如果您仍然不理解它的全部含义,请不要害怕。在查看了几个示例后,它会变得更容易理解。
在 Java 中,AOP 在 AspectJ 和 Spring 框架中实现。在 .NET 中,有 PostSharp(非免费)、NConcern 以及一些其他框架(不太流行且不易使用)可以实现几乎相同的功能。
也可以使用 RealProxy
类来实现 AOP。您可以在此处找到一些如何执行此操作的示例。
示例 1:面向切面编程:使用 RealProxy 类进行面向切面编程
本文还包含大量关于 AOP 是什么、装饰器设计模式 如何工作以及使用 AOP 实现日志记录和身份验证的示例。
示例 2:MSDN
不幸的是,这些示例存在一些显著的缺点。示例 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!");
这行代码的作用是:
decorated.MyMethod("Hello world!")
调用LoggingAdvice
的Invoke
方法,其中targetMethod
等于MyMethod
,args
等于一个包含一个元素的数组,该元素为 "Hello world!
"。LoggingAdvice
类的Invoke
方法记录MyMethod
方法名和输入参数(LogBefore
)。LoggingAdvice
类的Invoke
方法调用MyClass
的MyMethod
方法。- 如果方法调用成功,则记录输出参数和结果(
LogAfter
),并且Invoke
方法返回结果。 - 如果方法调用引发异常,则记录异常(
LogException
),并且Invoke
引发相同的异常。 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 个问题:
Calculator
类与日志记录耦合。松耦合(因为ILogger
是一个接口),但仍然耦合。每次更改ILogger
接口时,都会影响Calculator
。- 代码变得更复杂。
- 这违反了单一职责原则。
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());
}
}
结论
这段代码在我的情况中是有效的。如果您遇到代码无效的情况,或者对如何改进代码有任何想法,请随时通过任何方式与我联系。
就是这样——尽情享受吧!