.Net中的AOP系列之《將AOP作為架構工具》


返回《.Net中的AOP》系列學習總目錄


本篇目錄


本節的源碼本人已托管於Coding上:點擊查看

本文實驗環境:VS 2017 Community。


要有效地使用AOP,AOP自己的架構及其對大代碼架構的影響是要理解的重要概念。 當你在設計和實現一個架構時,PostSharp可以快速且自動地幫助你在編譯時識別錯誤。

直到現在,我們一直在狹隘地研究PostSharp和AOP:一次一個切面和一個類。現在從架構師的角度, 來看看PostSharp是如何與整個系統配合在一起的。 PostSharp包含了使架構師工作更簡單的工具,以及確保各切面本身都具有良好架構的工具。

在某些時候關於PostSharp,你可能關心的一件事就是我的所有例子都是將特性放在獨立的方法和屬性上,這也許看起來很繁瑣和重復,如果你在一個大的代碼庫中也必須這么做的話,確實很重復且繁瑣。 幸運的是,PostSharp並不要求你始終這樣做。 接下來會看看多播切面特性的方式,以便我們可以重用切面且不需要太多的特性重復。

因為PostSharp是作為編譯時工具實現的,它為我們開辟了編寫可以在正常編譯時間之后立即運行的代碼的大門。 我們可以利用這個機會編寫代碼,用於驗證:切面正在正確的地方使用,並且不會在運行時引起問題,以及整個項目的結構和架構。 這種方法會使得早早發現問題(或者我喜歡稱之為失敗更快,或最早失敗)。

我們也將借此機會執行切面的初始化。 如果你有昂貴的操作(如使用Reflection),那么最好地讓它在構建期間就完成不要等到運行時。

編譯時初始化和驗證

我們來看一下上一章的PostSharp構建過程,看看它如何適應普通的.NET構建過程。 回想一下,你有編譯時階段(代碼編譯成CIL)和運行階段(其中CIL被編譯為及時被執行的機器指令),見下圖。 編譯時的AOP工具,如PostSharp,又增加了一個步驟(后編譯器),並在編譯之后但在執行之前修改CIL。

PostSharp為你編寫的每個方面執行幾個步驟。 每個方面都是使用aspect的構造函數實例化。 PostSharp然后執行驗證步驟(調用你切面的CompileTimeValidate方法)來檢查切面是否正在正確使用。 然后PostSharp執行一個初始化步驟(調用切面的CompileTimeInitialize方法)來執行任何昂貴的計算,而不是等到運行時。 最后,PostSharp會獲得這個切面實例並將其序列化(到二進制流),以便可以稍后在運行時進行反序列化和執行。

下面看一下后期編譯器的詳細圖解,對應上面這段話的解釋:

本節重點介紹該過程的驗證和初始化步驟。 直到這個章節中,為了保持簡單,在例子中,我沒有定義任何CompileTimeValidate或CompileTimeInitialize代碼。 所有這些步驟仍然會執行,但是因為我們沒有定義CompileTimeValidate或CompileTimeInitialize,那些步驟沒有做任何事情。

使用CompileTimeValidate,PostSharp可以讓我們更快地失敗。 如果我們可以在編譯時驗證某些事情,那么就不必等到運行時才去發現錯誤或者獲得異常。CompileTimeInitialize可以讓我們提前處理代價高昂的事情。 如果在運行程序之前我們可以處理一個(可能代價高昂的)操作,那么就盡可能處理吧。

編譯時初始化

通常,一個切面需要知道一些關於它被用在了哪里的信息,例如方法名稱,一些參數信息或其他信息。所有這些信息可以由PostSharp API提供,該API使用Reflection填充這些信息到你的切面(例如,args參數)。 Reflection信息是我見過的初始化中最常見的事情,但是在整個程序執行之前可以獲得和實例化的其他任何昂貴的信息也是必須處理的,還是那句話,能在編譯時去做的,就不要等到運行時。

快速看個例子,我們來看一下基本的日志記錄切面(之前的章節中已經演示多次了)。 不是只記錄一個字符串,而是記錄它的方法名稱。 如果我使用PostSharp 的OnMethodBoundaryAspect,並在運行時這樣做,代碼如下:

using PostSharp.Aspects;
using System;

namespace AopInitialAndValidate
{
    [Serializable]
    public class MyLoggingAspect:OnMethodBoundaryAspect
    {
        public override void OnEntry(MethodExecutionArgs args)
        {
            Console.WriteLine($"The method name is {args.Method.Name}");
        }
    }
}

using static System.Console;
namespace AopInitialAndValidate
{
    class Program
    {
        static void Main(string[] args)
        {
            var mm = new MyClass();
            mm.MyMethod();
            Read();
        }
    }

    class MyClass
    {
        [MyLoggingAspect]
        public void MyMethod()
        {
            WriteLine("Now in MyMethod...");
        }
    }
}


如果你檢查args.Method,你會注意到它是一個類型為MethodBase的對象,位於System.Reflection中。 雖然這對我們的例子來說關系並不大,因為只有一個邊界方法,但是在大型應用程序中,每次切面都使用反射會使得性能大打折扣。

但是在運行時付出這個性能代價是不必要的。 方法名稱在程序運行時不會改變,所以為什么不在PostSharp的后期編譯過程中,將方法的名稱在編譯時確定下來呢? 在下面,我們覆蓋此切面的CompileTimeInitialize並將該方法名稱存儲在私有字符串字段中的。 在OnEntry中,使用該字符串字段而不是args.Method.Name。

using System.Reflection;

namespace AopInitialAndValidate
{
    [Serializable]
    public class MyLoggingAspect:OnMethodBoundaryAspect
    {
        private string _methodName = string.Empty;

        public override void OnEntry(MethodExecutionArgs args)
        {
            //這次直接使用私有字段而不是反射
            Console.WriteLine($"The method name is {_methodName}");
        }
        public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
        {
            _methodName = method.Name;//存儲方法名稱到私有字段
        }
    }
}

PostShrp 許可

關於PostSharp許可的一個注意事項:如果你使用Express(免費)版本,那么這個初始化對性能沒有太大的影響,因為它沒有進行任何優化,args.Method始終使用Reflection來填充OnEntry。 但是,遵循使用CompileTimeInitialize這一實踐仍然是個好主意,以防你最終需要AspectShaper的完整版本可以提供的切面優化。

使用Reflection只是在編譯時可以執行的一種操作而不是運行時。 如果要執行其他緩慢或昂貴的操作,那么應該在CompileTimeInitialize中進行處理。

因為PostSharp已經在編譯時運行這個初始化代碼,為什么不借此機會在運行一些這個切面的驗證代碼呢?

切面驗證的正確用法

在PostSharp中使用CompileTimeValidate可以讓我們檢查切面應用到具體代碼的上下文,以及確保在運行時正確運行。要使用CompileTimeValidate,只需要在切面中重寫該方法。

下面新建一個控制台項目,添加postsharp nuget包,代碼如下:

using System;
using PostSharp.Aspects;
namespace AopValidate
{
    [Serializable]
    public class MyLocationAspect:LocationInterceptionAspect
    {
        public override void OnGetValue(LocationInterceptionArgs args)
        {
            //當讀取屬性值時會打印
            Console.WriteLine("Now in getter of property");
            args.ProceedGetValue();
        }
    }
}

using static System.Console;
namespace AopValidate
{
    class Program
    {
        [MyLocationAspect]
        public static string MyName { get; set; }
        static void Main(string[] args)
        {
            MyName = "farb";
            Read();
        }
    }
}

但是,我這里有個奇怪的需求,只有當屬性名稱為“farb”(忽略大小寫)時,才能給該屬性使用該切面,否則,認為切面使用不當,這時就會編譯失敗。我將重寫CompileTimeValidate方法(代碼如下)並檢查位置的名稱。 如果不是“farb”,那么我將使用PostSharp API的Message類寫出錯誤消息。

        public override bool CompileTimeValidate(LocationInfo locationInfo)
        {
            if (!locationInfo.Name.ToLower().Equals("farb"))
            {
                //locationInfo是關於屬性的反射信息
                //Message是PostSharp提供的API
                //SeverityType是一個定義消息嚴重級別的枚舉
                Message.Write(locationInfo, SeverityType.Error, "MyCompileErrorCode01", "The name of property must be farb,not case sentive");
                return false;
            }
            return true;
        }


此時,直接編譯會報錯,因為"MyName".ToLower().Equals("farb")的值是false,因此會出現編譯錯誤。farb,Farb,FarB等都是編譯成功的。

我不會在這里全面地介紹這個PostSharp功能,但我會指出幾件有趣的事情。 首先,請注意CompileTimeValidate返回一個bool。 如果CompileTimeValidate返回false,該切面將不會被應用那個指定的位置,即切面使用失效。 如果我把aspect屬性放在100個屬性上,而且只有一個被命名為“farb”,那么該切面只會被應用一次。

第二,請注意,我選擇的SeverityLevel是“錯誤”。通過這樣做,我告訴PostSharp編寫一個編譯器錯誤,因此Visual Studio也會當作錯誤處理。 如果我使用“警告”的嚴重級別,消息將顯示為警告,並且項目會繼續編譯。 根據我的經驗,警告經常被忽略,所以一般我喜歡堅持使用“錯誤”。另一個關於錯誤消息值得注意的是:后期編譯器不會停止在它發現的第一個錯誤 , 它將繼續處理各切面並寫出每個消息。

最后,錯誤代碼和消息可以是任何你想要的字符串。當PostSharp沒在Message中包含行號和文件名時,將錯誤代碼和消息描述的越詳細並且盡可能讓任何人遇到這些CompileTimeValidate錯誤可以輕松找到違規代碼,是一個很好的實踐。錯誤消息中包含完整類名,完整屬性類型和屬性名稱,創建有意義的錯誤代碼也是好的實踐(當然,同時使用類似的信息方法切面再好不過了)。

現在你已經看到了一些CompileTimeInitialize和CompileTimeValidate的基本示例。 現在看一個真實的案例切面。

真實案例:復習線程

我們來回顧一下第3章中的線程示例。回想一下,我們創建了一個WorkerThread方面來等待另一個線程,以及一個UIThread切面,以確保
從該WorkerThread線程調用的任何UI代碼都在UI線程上運行。 我們再來一個特別看看UIThread,並尋找這個切面可能引起的問題:

using PostSharp.Aspects;
using System;
using System.Windows.Forms;
namespace RevisitThread
{
    [Serializable]
    public class UIThread:MethodInterceptionAspect
    {
        public override void OnInvoke(MethodInterceptionArgs args)
        {
            var form =(Form) args.Instance;
            if (form.InvokeRequired)
            {
                form.Invoke(new Action(args.Proceed));
            }
            else
            {
                args.Proceed();
            }
        }
    }
}

這個方面取決於args.Instance是一個Form類型的對象。 如果不是,怎么辦?然后,將args.Instance強制轉換為Form將導致InvalidCastException(無效轉換異常)拋出。 我們可以添加運行時檢查以避免這種情況。 不使用強制轉換,我們可以使用C# as運算符來轉換對象,然后檢查以確保在嘗試調用InvokeRequired和Invoke之前,該轉換是有效的。 當使用as,如果該轉換無法執行,那么返回一個null,也不會有異常拋出:

var form = args.Instance as Form;
if (form==null)
{
    args.Proceed();
}
if (form.InvokeRequired)
{
    form.Invoke(new Action(args.Proceed));
}
else
{
    args.Proceed();
}


這樣做至少不會使切面拋出異常。 但它仍然有問題:UIThread被用在沒有從Form繼承的類上怎么辦?是否有一個新的團隊成員
不熟悉線程工作的? 有人試圖使用這個切面在Windows Forms之外的UI框架? 如果我們只在運行時檢查,它只解決了異常問題,但其他問題呢?

不要拖延,馬上使用這個切面處理這樣的問題。 這次不使用as轉換,我們來使用CompileTimeValidate以確保在編譯時,UIThread切面始終用於Form類中的方法。 如果不是,我們會寫一個有關UIThread正在被不正確使用的信息錯誤消息,並阻止構建成功。

        public override bool CompileTimeValidate(MethodBase method)
        {
            //使用IsAssignableFrom檢查DeclaringType是否從Form繼承
            if (typeof(Form).IsAssignableFrom(method.DeclaringType))
            {
                return true;
            }
            else
            {
                string errMsg = $"UIThread must be used in Form.[Assembly:{method.DeclaringType.Assembly.FullName},Class:{method.DeclaringType},method:{method.Name}]";
                PostSharp.Extensibility.Message.Write(
                    method,
                    SeverityType.Error,
                    "UIThreadFormError01",
                    errMsg
                    );
                return false;
            }
        }


在該切面驗證失敗的地方,我寫了一個解釋錯誤的消息,包括程序集,類和方法名稱。 你還可以寫出參數或你認為可能的任何其他有助於找到問題的信息。

分不清哪一個Message?

如果你正在使用Windows Forms應用程序,請確保你不要將System.Windows.Forms.Message與PostSharp.Extensibility.Message混淆。為了清楚起見,我在上一個例子中寫了全命名空間類名。

現在,不必等待程序崩潰或其他后果,你可以使用此CompileTimeValidate代碼來更快地失敗。 我將把這個切面放在沒有繼承Form的類(我將把一個UIThread特性放在類NotaWindowsForm中的一個方法中MyMethod),看看當我嘗試構建時會發生什么:

    class NotaWindowsForm
    {
        [UIThread]
        public void MyMethod()
        {

        }
    }

編譯失敗,如下所示:

使用CompileTimeValidate並不能消除關於AOP使用的交流和團隊合作的需要,它會使得交流發生得更快,更容易和更便宜的解決。 你想做的最后一件事就是部署到生產環境前一天進行一次討論。

編譯時驗證是PostSharp所有版本中可用的更有趣和強大的功能之一。 事實上,它使得許多開發人員編寫只包含編譯時驗證的切面。 這些驗證切面可以用於檢查項目的代碼,並將其發現的任何錯誤視為編譯器錯誤。它們沒有切入點,並且不包含程序啟動后運行的代碼。 這個
是一種常見的技術,PostSharp開發人員決定將其做成一個稱為架構約束的一流功能。

架構約束

在PostSharp中,架構約束功能可幫助你為項目編寫完整性檢查。 可將其視為項目架構的單元測試。

PostSharp 許可
在我們進入本章之前,我們來聲明一些許可和技術問題。 此功能(架構約束)與AOP並不嚴格相關。 它在PostSharp的免費Express版本也並不可用,但展示后期編譯器IL操縱的強大仍然是有趣的事情。 上一節的編譯時驗證可用於所有版本的PostSharp,包括免費版本。

在本節中,我將給出PostSharp允許你創建的約束類型的概述,我會向你展示一個真實的例子,如果你使用NHibernate,架構約束可能會派上用場。

強制架構

架構約束的主要思想是你可以編寫其他代碼以自動的方式檢查你的項目代碼。 我們都知道,即使一個項目編譯,這並不意味着它不會失敗。 如果在編譯時,我們可以運行一些額外的檢查,那么我們會更早地了解問題(繼續本章的主題--失敗更快)。

PostSharp使我們能夠編寫兩種不同類型的架構約束:標量和引用。 這種分離部分是語義的,部分是技術性的。 這兩種類型的約束都可以在編譯時執行C#編譯器本身並沒有提供的規則,但引用約束都會在PostSharp處理引用代碼元素的程序集檢查的。

  • 標量約束
    標量約束是一個簡單的約束,它是為了影響單個獨立的代碼段。 這最像在切面中使用CompileTimeValidate方法(除了沒有切面部分)。

例如,當你使用NHibernate時,實體的所有屬性都必須是標記為virtual(在前面的章節中涵蓋了NHibernate使用Castle DynamicProxy)。 我經常忘記給屬性標記virtual,直到我運行程序並使用實體的時候才發現這問題。

我寧願在編譯時收到一條錯誤信息。 在下一小節中我們會更詳細地看看,介紹一個NHibernate virtual約束的實際示例。

示例不限於NHibernate。 如果你之前使用過WCF,那么也許你已經創建了一個DataContract類,添加了一個新的屬性,並忘記在其上放置DataMember特性。 或者,你是否創建了一個ServiceContract接口,並忘記將OperationContract特性放在已添加到其中的新方法中? 這些是令人沮喪的問題,正常的C#編譯器不會檢測到,但是標量約束可以早期檢測到。

  • 引用約束
    引用約束是更廣泛的架構約束形式。 引用約束意味着在組件,引用和關系之間實施架構設計。 這種約束形式對於架構師來說可能是有用的,特別是如果你正在開發API。

PostSharp帶有三個開箱即用的約束,你可以在一些特定的情況下使用:ComponentInternal,InternalImplements和Internal。見下圖:

當然,你完全可以編寫自己的引用約束。 我經常遇到的一個煩惱是,sealed可能被濫用,從而限制了可擴展性。 有時有一個很好的理由使用sealed,但不是經常。 它也可能使測試困難。 因此,為了防止sealed潛行在一個項目中,我創建了一個稱為“Unsealable”的引用約束。

要寫這個約束,我創建了一個名為Unsealable的類,它繼承自PostSharp的引用約束代碼基類。 在這個類里面,我覆蓋了ValidateCode方法,它接收一個目標對象以及一個程序集 。 我掃描整個程序集以查找從目標類派生的密封類。 我還需要使用一個特性(MulticastAttributeUsage)告訴PostSharp這個約束究竟應該准確應用到哪個項目。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using PostSharp.Constraints;
using PostSharp.Extensibility;

namespace ArchitectureConstraints
{
    [Serializable]
    [MulticastAttributeUsage(MulticastTargets.Class)]
    public class Unsealable : ReferentialConstraint
    {
        public override void ValidateCode(object target, Assembly assembly)
        {
            //目標類型是一個標記為Unsealable的類
            var targetType = (Type)target;

            List<Type> sealedSubClasses = assembly.GetTypes()//獲得所有類型
                .Where(t => t.IsSealed)//類型是密封的
                .Where(t => targetType.IsAssignableFrom(t))//類型繼承自目標類型
                .ToList();

            //遍歷所有密封子類
            sealedSubClasses.ForEach(s =>
            {
                //為每個類輸出錯誤信息
                Message.Write(s,SeverityType.Error,
                    "Unsealable001",
                    $"Error on {s.FullName}.Subclasses of {targetType.FullName}  cannot be sealed."
                    );
            });
        }
    }
}


任何使用該Unsealable特性標記的類意味着任何其他從它繼承的類不能是密封的,否則,將導致一個錯誤,從而阻止構建完成。

接下來演示這個約束。 編寫一個名為MyUnsealableClass的類; 把Unsealable 特性放在它上面; 編寫另一個從MyUnsealableClass繼承的類叫TryingToSeal; 使TryingToSeal成為一個密封類;並嘗試編譯。

namespace ArchitectureConstraints
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }

    [Unsealable]
    public class MyUnsealableClass
    {}

    public sealed class TryingToSeal:MyUnsealableClass
    {}
}

注意:如果這里你使用的是Express版本,那么編譯是成功的,因為此功能在Express版本不可用。這里,我安裝了一個45天試用的Ultimate版本,編譯就報錯了。

再強調一次,這些架構約束不能消除團隊溝通的需要。 作為一名架構師,如果你不希望開發者密封他們的類,那么這會使得適當使用C#密封關鍵字發生得更早。總之, 失敗越快越好。

說到快速失敗,我們來看一下在NHibernate中創建一個實體類,處理virtual關鍵字的標量約束。

真實案例:NHibernate

NHibernate實體的每個屬性必須是virtual的。 但在創建新的屬性和更改數據訪問代碼,我經常忘了使用virtual修飾屬性。

必須是virtual的嗎?

如果禁用延遲加載,則可以具有不是虛擬的NHibernate實體屬性。 但是延遲加載是NHibernate的一個重要功能,所以默認情況下是NHibernate會假設你想要懶加載,除非你在.hbm映射文件中進行了配置禁用懶加載。

C#編譯器不關心(也不應該關心)這個問題,應該讓我繼續編譯和運行代碼 ,但當我嘗試建立一個會話工廠時,會得到非常熟悉的InvalidProxyTypeException。

現在我不得不停止我正在做的事情 - 我正在嘗試的任何功能或者我正在演示的網站,然后將virtual添加到屬性。 並希望我沒有忘記任何其他的屬性。 因為如果我還遺忘了給其他屬性添加virtual,那么我必須再修正多次。

我想避免這種煩惱: 我寧願失敗得更快。 讓我們創建一個PostSharp標量約束,使編譯時就意識到這些錯誤。

編寫標量約束類似於編寫引用約束。 我創建一個繼承自PostSharp ScalarConstraint基類的類,並覆蓋ValidateCode方法。 在該方法中,獲取目標類型的所有屬性(在我們的例子中,目標是每個實體類的Type)。對於目標類型的每個屬性,檢查以確保它是虛擬的。 如果不是,寫一個消息。 再一次使用MulticastAttributeUsage特性來指示此約束可以應用到哪一個元素。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PostSharp.Constraints;
using PostSharp.Extensibility;
using System.Reflection;

namespace ArchitectureConstraints
{
    [Serializable]
    [MulticastAttributeUsage(MulticastTargets.Class)]//約束用在類上
    public class NhEntity : ScalarConstraint
    {
        public override void ValidateCode(object target)//注意這次只有一個參數,沒有assembly參數
        {
            //因為目標是類,所以可轉成Type類型
            var targetType = (Type)target;

            //獲取實例的所有公共屬性
            var properties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => !p.GetGetMethod().IsVirtual);//且屬性不是虛擬的

            foreach (PropertyInfo p in properties)
            {
                //對於沒有virtual修飾的屬性,打印錯誤消息
                Message.Write(targetType, SeverityType.Error,
                    "NhVirtual001",
                    $"Property {p.Name} in entity class {targetType.FullName} is not virtual .");
            }
        }
    }
}

要使用此約束,可以在領域模型的每個實體類上放置一個[NhEntity]特性。 但是如果你又創建了一個新的實體並忘記在上面放置屬性,該怎么辦? 相反,我將使用PostSharp的另一個功能:特性多播。此功能允許我一次指定多個類。

我們將在下一節中探討多播的工作原理,但這里先來一點預習。 如果我將所有的NHibernate模型實體都放在相同的命名空間(例如,NHibernateExample.Entities)中,我可以使用一個程序集指令來多播這個NhEntity標量約束:

[assembly: NHEntity(AttributeTargetTypes =
"NHibernateExample.Entities.*")]

你可以將其添加到項目中的標准AssemblyInfo.cs文件中,但可能一個更好的想法是把它放入自己的文件,如AspectInfo.cs。 現在每一個在NHibernateExample.Entities命名空間中的類將具有NHEntity標量約束。 在NHibernateExample.Entities命名空間中創建一本Book類,但保留一個屬性不是虛擬的:

using System;
namespace NHibernateExample.Entities
{
    public class Book
    {
        public virtual Guid Id { get; set; }
        public string Name { get; set; }//這個不小心忘記了virtual
        public virtual string Publisher { get; set; }
        public virtual decimal Price { get; set; }
    }
}


編譯,會出現下面的錯誤,這樣就使得我們在運行程序前可以有機會進行修改。

特性多播不限於架構約束,並且對於正常的PostSharp切面來說可能也很有用,以便最大限度地減少重復使用。 而與架構約束不同,特性組播在免費的Express版本中也可以使用。

多播

在第1章中,我將切入點定義為放置切面的“地方”。 可以將切入點認為是描述切面可以放置的語句(通過向流程圖中添加額外的箭頭來描述它,如下圖,此圖第一章中已經貼過):

到目前為止,所有的例子都是簡單而狹隘的切入點:一兩個成員,也可能是整個類。 所以為了保持簡單,我把幾個特性放在代碼中。 實際上,許多切面具有更廣泛的切入點,對每一個類或方法使用特性可能成為分散樣板的另一種形式(雖然較少侵入性和較少糾纏)。 如果你有很多使用切面的代碼,或代碼經常更改,我建議你不要反復使用特性。

這就是PostSharp特性多播功能所能解決的問題(一些其他的AOP工具有稱為動態切入點的功能,這是類似的)。 我們可以在三個層面應用PostSharp切面:

  1. 單個方法或位置級別
  2. 在類級別(會應用到該類的所有成員)
  3. 在程序集級別(會應用到該程序集的多個類和成員)

之前看到的例子基本是將切面特性用到單個方法或屬性上,這樣獲得了應用切面所在代碼的最大靈活性和控制。下面,我們開始做一些將特性放到類和程序集上的例子。

類級別多播

如果你寫了一個LocationInterceptionAspect並在類級別應用它,默認情況下將攔截該類中的所有位置。

如果你編寫了方法切面(OnMethodBoundaryAspect或LocationInterceptionAspect)並將其應用於類級別,則默認情況下將應用於該類中的所有方法。 這種方法將等同於使用特性四次,每個方法使用一次。見下圖:

當使用切面作為特性時,在特性的構造函數中你將有很多關於多播的配置選項可用。 這些都包含在PostSharp文檔中,但這里有一些顯着的例子:

  • AttributeExclude : 從接收到的多播特性中有選擇地排除一些方法
  • AspectPriority: 定義使用切面的順序(C#中特性的順序本質上是不確定的)
  • AttributeTargetElements: 選擇使用切面的目標類型

為了演示,我們來編寫LogAspect類,它只會報告哪些方法正在應用該切面。 一旦我們有了這個切 面,就可以改變配置選項,看看會發生什么:

using PostSharp.Aspects;
using System;
using static System.Console;
namespace ArchitectureConstraints.Multicasting
{
    [Serializable]
    public class LogAspect:OnMethodBoundaryAspect
    {
        public override void OnEntry(MethodExecutionArgs args)
        {
            WriteLine($"Aspect was applied to {args.Method.Name}");
        }
    }

    [LogAspect]
    class MyClass
    {
        public void Method1() { }
        public void Method2() {
            Method3();
        }
        private void Method3(){}
    }
}

class Program
{
    static void Main(string[] args)
    {
        var m = new MyClass();
        m.Method1();
        m.Method2();
        Read();
    }
}


定義切面LogAspect,並將它直接使用到MyClass上,運行程序,結果如下,可以看到該切面應用到了MyClass的構造函數上和Method1/2/3上:

如果想將LogAspect應用到除了Method3之外的任何方法上,可以使用AttributeExclude進行設置:

[LogAspect]
class MyClass
{
    public void Method1() { }
    public void Method2() {
        Method3();
    }
    [LogAspect(AttributeExclude =true)]//從多播中排除這個方法
    private void Method3(){}
}

再次運行程序,可以看到切面LogAspect沒有應用到Method3上:

使用多個切面可能是常見的情況。 應用這些切面的順序可能很重要。 例如,你可能希望在同一類上使用緩存的切面和安全性的切面。 下面,我使用MyClass演示這種場景:

using PostSharp.Aspects;
using System;
using static System.Console;
namespace ArchitectureConstraints.Multicasting
{
    [Serializable]
    class AnotherAspect:OnMethodBoundaryAspect
    {
        public override void OnEntry(MethodExecutionArgs args)
        {
            WriteLine($"Another aspect was applied to {args.Method}");
        }
    }
}

    //演示AspectPriority
    [LogAspect(AspectPriority =1)]//LogAspect有最高的優先級
    [AnotherAspect(AspectPriority =2)]
    class MyClass
    {
        public void Method1() { }
        public void Method2()
        {
            Method3();
        }
        private void Method3() { }
    }


當你使用C#編譯器時,你無法保證這些特性按照你指定的順序應用。要強制應用順序,可以使用AspectPriority設置。

LogAspect的優先級高於AnotherAspect,所以它將首先被應用。 如果我交換這個優先級順序,AnotherAspect將首先被應用。

使用AttributeTargetElements可以更詳細地指出要多播的元素。 此設置使用MulticastTargets枚舉,其中包括諸如Method,InstanceContructor和StaticConstructor。 如果我選擇InstanceConstructor,該切面僅適用於構造函數。

    /// <summary>
    /// 演示AttributeTargetElements
    /// </summary>
    [LogAspect(AttributeTargetElements =PostSharp.Extensibility.MulticastTargets.InstanceConstructor)]
    class MyClass
    {
        public MyClass() { }
        public MyClass(int n) { }
        public void Method1() { }
        public void Method2()
        {
            Method3();
        }
        private void Method3() { }
    }

可以看到,切面只用到了實例構造函數上:

通過在類級別進行多播,你可以獲得合理的默認值(適用於所有內容)和靈活配置,如果需要,你可以使用單個成員。 在程序集級別進行多播時,可以使用這些相同的配置選項。

程序集級別多播

在本章前部分,你已經看到了如何在程序集級別使用特性多播的方式。 我們設置PostSharp將NhEntity標量約束特性應用於某個命名空間中的每個類:

[assembly: NhEntity(AttributeTargetTypes ="NHibernateExample.Entities.*")]

雖然這不是一個切面,但是因為切面和約束都是特性,所以它們都可以組播。

我們從基礎開始吧。 要將一個切面應用於整個程序集,請使用語法[assembly:MyAspect]。 此語法將該切面應用到整個程序集中每個有效的目標。

要縮小范圍,請使用特性構造函數中的PostSharp配置選項。 你在程序集級別上可以使用與類級別相同的選項。但在程序集級別,AttributeTargetTypes很多設置變得更有用,因為你可以使用它來應用一個切面到多個類或命名空間。

在NhEntity示例中,我將目標設置為具有通配符的命名空(NHibernateExample.Entities *)。 除了通配符( *),你可以使用正則表達式或類的確切名稱。

你可以在命名空間或類型層次結構的任何部分使用通配符。 如果我有多個實體命名空間(例如Sales.Entities和Support.Entities),我也可以在命名空間的第一部分使用通配符:

[assembly: MyAspect(AttributeTargetTypes = "*.Entities.*)]

當你想要使用約定來確定哪些切面被應用時,使用正則表達式可能會很有幫助。 如果我建立了一個約定,訪問數據庫的每個類都是以Repository結尾命名的(AbcRepository,DefRepository等),並且我想將事務切面應用於每個repository類,我可以使用正則表達式:

[assembly: TransactionAspect(AttributeTargetTypes= "regex:.*Repository$")]

不要濫用正則表達式 - 保持簡單。 使用明確和合理的慣例,並確定表達式將不會拾取它們不應該的任何目標。 如果一個正則表達式比這個例子復雜得多,你可能要重新思考你的架構或組織。

你還可以通過使用AttributeTargetMembers配置來多播到具有特定名稱的各個成員。 適用相同的規則:你可以使用確切的名稱,通配符或正則表達式。

如果我想將日志記錄切面應用於名稱中包含Delete的所有方法,我可以使用AttributeTargetMembers:

[assembly: LogAspect(AttributeTargetMembers="*Delete*")]

如果你的架構包含使用命名空間和/或明智使用約定命名的良好結構,則可以通過使用這些程序集級別切面完全控制切面被用在什么地方。 使用所有配置選項,還可以在必要時定義一些復雜的切入點。

使用程序集級別特性來幫助定義切入點的好處是,你不需要通過到處使用特性使得代碼混亂。 此外,你將所有切入點定義在一個方便的文件(如AspectInfo.cs)中,這樣可以更輕松地查看哪些切面被使用。

小結

在過去三章中,我強調了使用.NET中兩個主要AOP工具的兩種途徑之間的主要區別。 它們是兩個出色的框架,比較和對比它們有助於闡明AOP的基本概念和兩種途徑之間的重大權衡。

在本章中,我簡要介紹了將多個切面應用於相同代碼。 我們使用AspectPriority來確定應用程序的順序。 在下一章中,我們將回顧兩種工具,我將展示如何使用DynamicProxy進行此操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM