使用 C# 和 PostSharp 进行面向方面编程






4.79/5 (44投票s)
本文讨论了 AOP 的基本概念,并使用 PostSharp 在 C# 应用程序中实现 AOP 概念。
引言
在本文中,我使用了 PostSharp,它是编译时代码织入最友好的工具之一,用于开发一些非功能性需求,如异常处理、日志记录、执行时间测量和事务管理。它允许在 C# 代码中使用 AOP,语法非常简单直观。我见过许多开发人员在他们的应用程序中重复上述代码块,导致代码难以理解和修改。此外,AOP 补充了 OOP,为了消除出现在每个类中的混合代码,强烈推荐 AOP。我演示了如何使用 AOP 来减少代码行数,并创建易于阅读和管理的程序。
注意:在运行示例代码之前,请记住在 app.config 中设置 TestConnectionString
,并在您的系统中运行 Script.Sql。
要求
要跟随本文进行学习,您需要安装以下软件
- Visual Studio 2010 或更高版本,以及 .NET 4.0 或更高版本。Visual Studio Express 版本也支持 PostSharp 的后编译支持,因此您编写的 Aspect 可以正常工作,但 PostSharp 的 IDE 扩展不适用于 Express 版本。
- PostSharp 2.1。可以从 http://www.sharpcrafters.com/postsharp/download 下载。
- Sql Server 200x 或 Sql Express 版本.
- 本文提供的源代码.
背景
在 AOP 中,有几个术语是必须了解的。定义这些术语比实现它们更难。但是,我将简要定义它们,并在示例代码中阐明它们的职责。我将讨论最重要的几个。
1. 横切关注点 (Crosscutting Concerns)
指的是每个类中都包含的混合代码。例如,在下面的代码中,异常处理和跟踪块被称为混合代码,可以移入一个 Aspect。
void AddRecord(string firstName, string lastName, string email)
{
Try
{
//Code To Add a Record...
}
Catch(Exception ex)
{
Trace.WriteLine("Exception during transaction: " + ex.ToString());
}
}
2. Aspect (切面)
Aspects 在编译时或运行时修改其他类或对象的行为。它们被称为类或对象的织入。在我们将要讨论的示例中,Aspects 被定义为属性,并声明在方法或类的顶部,以便为这些方法或类添加某些功能。
3. Joinpoint (连接点)
我们可以将代码中调用另一个方法的任何点视为 Joinpoint
。
4. Pointcut (切入点)
一种在我们的代码中定义 Joinpoint
s 的方式。Pointcut
还包含一个通知 (advice),该通知在到达 joinpoint
时发生。因此,如果我们为一个特定的方法调用定义一个 Pointcut
,当调用发生或 joinpoint
被调用时,它会被 AOP 框架拦截,并执行 pointcut
的通知。
5. Advice (通知)
在我们的代码中,当到达一个 Joinpoint
时,我们将调用另一个方法或多个方法。这些方法被视为 Advices。
Using the Code
我们的示例是一个 C# 控制台应用程序。从下面的图片可以看出,我们需要添加对 PostSharp.Dll 的引用。此外,我们的代码中有四个不同的 Aspect(在 Aspects 文件夹中),如下所示。
首先,我将讨论 Timing Aspect。在许多应用程序中,开发人员或用户计算代码执行时间非常重要。在传统的处理方法中,我们通常使用以下代码来计算此值
_StopWatch = Stopwatch.StartNew();
ExecuteSomeCode();
Console.WriteLine(string.Format("It took {0}ms to execute", _StopWatch.ElapsedMilliseconds));
但是,在每个应用程序中,我们可能需要在整个应用程序中重复这样的代码,以便在不同方法中获取经过时间。AOP 可以帮助我们减少这类代码。在此场景下,我们将创建一个 Aspect,并将其用作需要执行时间计量的每个方法顶部的属性,如下所示。
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method)]
public class TimingAspect : PostSharp.Aspects.OnMethodBoundaryAspect
{
[NonSerialized]
Stopwatch _StopWatch;
public override void OnEntry(PostSharp.Aspects.MethodExecutionArgs args)
{
_StopWatch = Stopwatch.StartNew();
base.OnEntry(args);
}
public override void OnExit(PostSharp.Aspects.MethodExecutionArgs args)
{
Console.WriteLine(string.Format("[{0}] took {1}ms to execute",
new StackTrace().GetFrame(1).GetMethod().Name,
_StopWatch.ElapsedMilliseconds));
base.OnExit(args);
}
}
注意:每个 Aspect 都应该是 Serializable
的,因此每个 Aspect
类都使用了 [Serializable]
属性。
这里我使用了 TimingAspect
Aspect 作为属性。
[TimingAspect]
static void LongRunningCalc()
{
//wait for 1000 milliseconds
Thread.Sleep(1000);
}
TimingAspect
继承自 OnMethodBoundaryAspect
类型。该类型在 详尽的 PostSharp 在线文档 中有更详细的解释,它提供了拦截方法代码并在其之前、之后、成功时或仅失败时执行代码的机会。OnEntry
、OnExit
是我们的 Advices,当调用并完成 LongRunningCalc
时将被调用。在这种情况下,代码中调用 LongRunningCalc
的点是 JoinPoint
。当 LongRunningCalc
被 PostSharp
调用时,OnEntry
被调用;当 LongRunningCalc
执行完成时,OnExit
被调用。以下是 LongRunningCalc
执行的结果。
LogAspect
与 TimingAspect
非常相似,它也继承自 OnMethodBoundaryAspect
,并重写了 OnEntry
、OnExit
,如下所示。
[Serializable]
public class LogAspect : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Console.WriteLine(Environment.NewLine);
Console.WriteLine("Entering [ {0} ] ...", args.Method);
base.OnEntry(args);
}
public override void OnExit(MethodExecutionArgs args)
{
Console.WriteLine("Leaving [ {0} ] ...", args.Method);
base.OnExit(args);
}
}
Exception
Aspect 略有不同,因为它继承自 OnExceptionAspect
类,并且必须重写 OnException
以响应 Joinpoint
抛出的每个异常。
[Serializable]
public class ExceptionAspect : OnExceptionAspect
{
public override void OnException(MethodExecutionArgs args)
{
Console.WriteLine(String.Format("Exception in :[{0}] ,
Message:[{1}]", args.Method, args.Exception.Message));
args.FlowBehavior = FlowBehavior.Continue;
base.OnException(args);
}
}
以下是我们应用程序中使用 ExceptionAspect
的示例。
[ExceptionAspect]
[LogAspect]
static void Calc()
{
throw new DivideByZeroException("A Math Error Occurred...");
}
RunInTransactionAspect
Aspect 继承自 OnMethodBoundaryAspect
,但为了支持事务管理,我们必须实现 OnEntry
、OnExit
、OnSuccess
和 OnException
。当 JointPoint
抛出异常时,整个事务将回滚,从而避免潜在的数据问题;否则,事务将完成。我们需要分别在 OnException
和 OnSuccess
方法中处理这些情况。
[Serializable]
[AspectTypeDependency(AspectDependencyAction.Order,
AspectDependencyPosition.After, typeof(LogAspect))]
public class RunInTransactionAspect : OnMethodBoundaryAspect
{
[NonSerialized]
TransactionScope TransactionScope;
public override void OnEntry(MethodExecutionArgs args)
{
this.TransactionScope = new TransactionScope(TransactionScopeOption.RequiresNew);
}
public override void OnSuccess(MethodExecutionArgs args)
{
this.TransactionScope.Complete();
}
public override void OnException(MethodExecutionArgs args)
{
args.FlowBehavior = FlowBehavior.Continue;
Transaction.Current.Rollback();
Console.WriteLine("Transaction Was Unsuccessful!");
}
public override void OnExit(MethodExecutionArgs args)
{
this.TransactionScope.Dispose();
}
}
注意:在上述代码中,使用了 AspectTypeDependency
属性来告知 PostSharp 在同时使用 RunInTransactionAspect
和 LogAspect
时,优先运行 LogAspect
。
这是我们的主代码及其执行结果。
class Program
{
static void Main(string[] args)
{
Calc();
LongRunningCalc();
//Adding records using a method
//which is run in a Transaction
AddRecord("Reza", "Ahmadi", "r_ahmadi_1983@yahoo.com");
AddRecord("Reza", "Ahmadi", "rahmadey@gmail.com");
AddRecord("X", "Y", "r_ahmadi_1983@yahoo.com"); //here an Exception will be thrown
Console.WriteLine("Press any key to continue");
Console.ReadKey();
}
[ExceptionAspect]
[LogAspect]
static void Calc()
{
throw new DivideByZeroException("A Math Error Occurred...");
}
[LogAspect]
[TimingAspect]
static void LongRunningCalc()
{
//wait for 1000 milliseconds
Thread.Sleep(1000);
}
[RunInTransactionAspect]
[LogAspect]
static void AddRecord(string firstName, string lastName, string email)
{
using (var cn = new SqlConnection
(ConfigurationManager.ConnectionStrings["TestConnectionString"].ConnectionString))
{
using (var command = cn.CreateCommand())
{
if (cn.State != ConnectionState.Open) cn.Open();
//command.Transaction = cn.BeginTransaction();
try
{
command.CommandText =
"insert into person values(@f,@l) select @@identity";
command.Parameters.AddWithValue("@f", firstName);
command.Parameters.AddWithValue("@l", lastName);
var personId = command.ExecuteScalar();
command.CommandText = "insert into emailAddress values(@p,@e)";
command.Parameters.AddWithValue("@p", personId);
command.Parameters.AddWithValue("@e", email);
command.ExecuteNonQuery();
//command.Transaction.Commit();
}
catch (Exception ex)
{
Trace.WriteLine("Exception during person-saving transaction: " +
ex.ToString());
//command.Transaction.Rollback();
throw;
}
}
}
}
}
注意: 上述代码中的注释掉的代码已被 Aspect 替换,因此被省略了。
结论
PostSharp 是在 .NET 框架中实现 AOP 最流行的方法之一。恰当应用时,PostSharp Aspects 可以减少代码混乱,并在不使代码因职责混杂而过于复杂的情况下,帮助维护架构标准和实践。在此示例中,PostSharp 实现了应用程序的重构,使得
- 日志记录代码已提取到单独的 Aspect 中,并从代码中显式移除
- 性能分析代码已提取到 Aspect 中,不再混杂
- 事务处理已提取并从代码中移除,清理了数据库执行代码
历史
- 2012 年 2 月:首次发布