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






4.95/5 (14投票s)
使用 RealProxy 类实现面向方面编程日志记录的示例
引言
面向切面编程 (AOP) 是一种非常强大的方法,可以避免样板代码并实现更好的模块化。其主要思想是在不修改现有代码本身的情况下,向现有代码添加行为(通知)。在 Java 中,这个思想在 AspectJ 和 Spring 框架中得到了实现。在 .NET 中,有 PostSharp(非免费)、NConcern 和一些其他框架(不太流行且不易使用)可以实现几乎相同的功能。
还可以使用 RealProxy
类来实现 AOP。您可以在此处找到一些关于如何实现它的示例
示例 1:面向切面编程:使用 RealProxy 类实现面向切面编程
示例 2:MSDN。
不幸的是,这些示例存在一些明显的缺点。示例 1 不支持 out 参数。示例 2 有一个限制。被装饰的类应该继承自 MarshalByRefObject
(如果不是您自己的类,这可能会有问题)。此外,这两个示例都不支持异步函数。好吧,严格来说是支持的。但我期望“after”代码在任务完成后执行,如果“after”代码尝试获取 Task
对象的结果(例如,在结果序列化期间),它会将异步代码变成同步的(不好 ☹)。
我尝试修复第一个示例。
在继续阅读之前
本文介绍如何修复 **Bruno Sonnino**(示例 1)提供的解决方案中的一些问题。该文章对代码的工作方式以及它解决的问题类型进行了很好的解释。请先阅读 面向切面编程:使用 RealProxy 类实现面向切面编程。
替代解决方案
还可以使用 DispatchProxy
来实现相同的功能。您可以在我的文章 使用 DispatchProxy 在 C# 中实现面向切面编程 中找到一个实现该功能的示例。
源代码
本文的代码以及用于 DispatchProxy
的示例(以及两者的单元测试)可以在 GitHub 上找到。
解决方案
此解决方案是日志记录实现的示例。代码可以在 此处 找到。
与原始代码的区别
- 添加了扩展方法
GetDescription
来记录异常数据 (Extensions.cs)。 DynamicProxy
类已重命名为LoggingAdvice
。- 构造函数被设为
private
。Static
函数Create
创建类实例并返回TransparentProxy
(LoggingAdvice.cs - 行 35-41)。这使得不可能创建LoggingAdvice
类的实例,因为只有由该类创建的代理将被使用。 LoggingAdvice
接收用于记录函数调用和错误的操作,以及用于序列化复杂类型值作为参数的函数 (LoggingAdvice.cs - 行 19-20)。- 添加了
TaskScheduler
作为可选参数,以支持使用不同的任务调度程序进行任务结果日志记录。默认将使用TaskScheduler.FromCurrentSynchronizationContext()
(LoggingAdvice.cs - 行 36)。 - 添加了函数
LogException
、LogBefore
和LogAfter
来记录相应的数据 (LoggingAdvice.cs - 行 150-209)。 - 添加了
Try
/catch
块来处理logInfo
函数抛出异常的情况 (LoggingAdvice.cs - 行 53-61, 99-107)。 - 如果函数的返回值是
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 个问题:
Calculator
类与日志记录耦合。松耦合(因为ILogger
是一个接口),但仍然耦合。每次更改此接口时,都会影响Calculator
。- 代码变得更复杂。
- 这违反了单一职责原则。
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());
}
}
}
结论
此代码适用于我的情况。如果您有任何代码无效的示例,或者此代码可以改进的地方,请随时以任何方式与我联系。
就是这样——尽情享受吧!