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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (44投票s)

2012年2月28日

CPOL

5分钟阅读

viewsIcon

176956

downloadIcon

3813

本文讨论了 AOP 的基本概念,并使用 PostSharp 在 C# 应用程序中实现 AOP 概念。

引言

在本文中,我使用了 PostSharp,它是编译时代码织入最友好的工具之一,用于开发一些非功能性需求,如异常处理、日志记录、执行时间测量和事务管理。它允许在 C# 代码中使用 AOP,语法非常简单直观。我见过许多开发人员在他们的应用程序中重复上述代码块,导致代码难以理解和修改。此外,AOP 补充了 OOP,为了消除出现在每个类中的混合代码,强烈推荐 AOP。我演示了如何使用 AOP 来减少代码行数,并创建易于阅读和管理的程序。

注意:在运行示例代码之前,请记住在 app.config 中设置 TestConnectionString,并在您的系统中运行 Script.Sql

app.config needed to be config

Sql Query to create sample tables

要求

要跟随本文进行学习,您需要安装以下软件

  • 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 (切入点)

一种在我们的代码中定义 Joinpoints 的方式。Pointcut 还包含一个通知 (advice),该通知在到达 joinpoint 时发生。因此,如果我们为一个特定的方法调用定义一个 Pointcut ,当调用发生或 joinpoint 被调用时,它会被 AOP 框架拦截,并执行 pointcut 的通知。

5. Advice (通知)

在我们的代码中,当到达一个 Joinpoint 时,我们将调用另一个方法或多个方法。这些方法被视为 Advices。

Using the Code

我们的示例是一个 C# 控制台应用程序。从下面的图片可以看出,我们需要添加对 PostSharp.Dll 的引用。此外,我们的代码中有四个不同的 Aspect(在 Aspects 文件夹中),如下所示。

Project Files

首先,我将讨论 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 在线文档 中有更详细的解释,它提供了拦截方法代码并在其之前、之后、成功时或仅失败时执行代码的机会。OnEntryOnExit 是我们的 Advices,当调用并完成 LongRunningCalc 时将被调用。在这种情况下,代码中调用 LongRunningCalc 的点是 JoinPoint。当 LongRunningCalc PostSharp 调用时,OnEntry 被调用;当 LongRunningCalc 执行完成时,OnExit 被调用。以下是 LongRunningCalc 执行的结果。

Using Aspect to report execution time

LogAspect TimingAspect 非常相似,它也继承自 OnMethodBoundaryAspect ,并重写了 OnEntryOnExit ,如下所示。

[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...");
}

Using Aspect to report execution time

RunInTransactionAspect Aspect 继承自 OnMethodBoundaryAspect ,但为了支持事务管理,我们必须实现 OnEntryOnExitOnSuccess 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 替换,因此被省略了。

Using all Aspects

结论

PostSharp 是在 .NET 框架中实现 AOP 最流行的方法之一。恰当应用时,PostSharp Aspects 可以减少代码混乱,并在不使代码因职责混杂而过于复杂的情况下,帮助维护架构标准和实践。在此示例中,PostSharp 实现了应用程序的重构,使得

  • 日志记录代码已提取到单独的 Aspect 中,并从代码中显式移除
  • 性能分析代码已提取到 Aspect 中,不再混杂
  • 事务处理已提取并从代码中移除,清理了数据库执行代码

历史

  • 2012 年 2 月:首次发布
© . All rights reserved.