AspectDNG を使ってインターフェースを追加実装する

.NET 用の AOP ツールである AspectDNG を使って、指定のクラスにインターフェースを追加実装する。

手順は以下の通り

  1. クラスとインターフェースの作成
  2. アスペクトの定義と適用
  3. テストクラスの作成と実行

クラスとインターフェースの作成

アスペクトの適用対象とするクラスとアスペクトで追加するためのインターフェースを作成する

public class Data {}
public interface IChecker
{
    void Check(int num, string msg);
}

これをビルドしてアセンブリを作成しておく

>csc /t:library /out:commonlib.dll Data.cs IChecker.cs

アスペクトの定義と適用

AspectDNG ではカスタムアトリビュートでアスペクトが定義でき、インターフェースを追加実装するには以下のカスタムアトリビュートを使用する事になる。

  • ImplementInterface でインターフェースを追加
  • Insert でメソッドの実装を追加
using System;
using DotNetGuru.AspectDNG.Joinpoints;

//Data クラスへインターフェースを追加する定義
[ImplementInterface("Data")]
public interface ICheckerAspect : IChecker
{
}

//アスペクトの定義クラス。
//Check メソッドのシグネチャが同じだけでは
//ウィーブ後にインターフェースの実装として認識されないようなので
//IChecker インターフェースを実装するようにしている
public class SimpleAspect : IChecker
{
    //Data クラスへ Check メソッドの実装を追加する定義
    [Insert("Data")]
    public void Check(int num, string msg)
    {
        Console.WriteLine("--- Check : {0}", this);
    }
}

ここで、SimpleAspect クラスの IChecker インターフェース実装宣言を外すと、実行時に System.TypeLoadException('Data' にあるメソッド 'Check' に実装が含まれていません)が発生するため注意が必要。

アスペクト定義クラスをビルドして、事前に作成しておいたアセンブリにアスペクトをウィーブする。

>csc /t:library /out:aspects.dll /r:aspectdng.exe;commonlib.dll SimpleAspect.cs
>aspectdng.exe commonlib.dll aspects.dll

aspectdng.exe の実行で commonlib.dll がアスペクト適用後のアセンブリで置換される。

テストクラスの作成と実行

Data クラスへ IChecker インターフェースの実装が追加されている事を確認するためのテストクラス作成

public class Tester
{
    public static void Main(string[] args)
    {
        //Data クラスへ IChecker インターフェースの実装が
        //追加されている事を確認
        IChecker checker = new Data() as IChecker;
        checker.Check(100, "test");
    }
}

実行結果は以下のようになり、インターフェースの実装が追加されている事が確認できる。

>csc /r:commonlib.dll;aspects.dll Tester.cs
>Tester.exe
--- Check : Data?

AspectDNG を使って private なフィールドにアクセスするメソッドを追加する

ターゲットクラスの private なフィールドにアクセスするメソッドを追加するには、アスペクトの定義クラスに Insert カスタムアトリビュートを使ってメソッドを実装すればよいだけだが、コンパイルを通すために アスペクトの定義クラスでターゲットクラスと同じフィールド名のダミーフィールドを定義する必要がある。

なお、Insert カスタムアトリビュートを使った追加ではプロパティの定義を追加する事ができない模様。

アスペクト適用対象のクラスを作成

using System;

public class Data
{
    private string name;

    public Data(String name)
    {
        this.name = name;
    }

    public void Print()
    {
        Console.WriteLine("name:{0}", this.name);
    }
}

アスペクト定義クラスを作成

ダミーの name フィールドを定義しておく事に注意。

using DotNetGuru.AspectDNG.Joinpoints;

public class SimpleAspect
{
    //コンパイルを通すためのダミーフィールド
    private string name;

    [Insert("Data")]
    public void set_Name(string value)
    {
        //実際は Data オブジェクトの name フィールドに値を設定する
        name = value;
    }

    //以下のようなプロパティ定義を使った追加はできない模様
    //[Insert("Data")]
    //public string Name { set{ name = value; } }
}

動作確認用のテストクラスを作成

public class Tester
{
    public static void Main(string[] args)
    {
        Data data = new Data("てすと");
        data.Print();

        data.set_Name("abc");
        data.Print();
    }
}

実行結果

>Tester.exe
name:てすと?
name:abc?

出力の最後に ? が付いている理由は不明。

AspectDNG の GAOP 機能の問題点

GAOP 機能は、アスペクトのウィービング時に内容を決定できる機能で実装の追加内容を文字列等で指定できる。

ただし、AspectDNG 1.0.3 の Generic アトリビュートを使った GAOP 機能では以下のような問題があり、現段階での有効利用は難しい印象がある。

  • フィールドの参照が適切に処理されない(ターゲットクラスに合わせてフィールドのオーナーを変更しない)

以下では、この問題の発生原因と解決案を説明する。

Generic カスタムアトリビュートを使ったアスペクト定義クラスの作成

まず、GAOP 機能を使うアスペクト定義クラスを作成する。

  • Generic カスタムアトリビュートでターゲットクラスを指定
  • Generic アトリビュートを付与したメソッドの戻り値としてターゲットクラスへ追加する実装を文字列で返す
using DotNetGuru.AspectDNG.Joinpoints;
using Mono.Cecil;

//GAOP 機能を使ったアスペクトの例
public class SimpleAspect
{
    [Generic("Data")]
    public static string Generator(TypeDefinition t)
    {
        return @"
            private string name;

            public void set_Name(string value)
            {
                name = value;
            }
        ";
    }
}

Generic カスタムアトリビュートを使った際のアスペクトの適用処理

Generic アトリビュートを使ったアスペクトのウィービングは以下のような手順で処理が実施される。

  1. Generic アトリビュートを付与したメソッドの戻り値が null で無ければ、その内容を実装とした動的なクラスを作成してコンパイル
  2. 動的に作成したクラスのメンバーをターゲットクラスに挿入

ここで、SimpleAspect クラスの Generator メソッドの戻り値から生成される動的クラスは以下のような内容になる。

//動的に生成されるクラスの例
public class AspectDngDynamicClassName1 {
    private string name;

    public void set_Name(string value)
    {
        name = value;
    }
}

AspectDngDynamicClassName1 のメンバー(フィールド・メソッドなど)がターゲットクラス(ここでは Data)へ挿入される際、set_Name メソッド内の name は AspectDngDynamicClassName1 のメンバーとして扱われる。

そのため、Data クラスの参照時に AspectDngDynamicClassName1 クラスが所属するアセンブリが見つからないというエラーが発生する事になる。(AspectDngDynamicClassName1 は一時的に作成されるクラスのためウィービング処理の後は存在しない)

問題の解決案

以上のようにフィールドのオーナーを変更しないままのメソッドをターゲットクラスに挿入してしまう問題は、実用的では無いものの、次のような手順で解決する事が可能である。

  • ildasm で IL を生成
  • IL を修正
  • ilasm でアセンブリを再生成

つまり、IL にして修正すればよいという事。

ildasm で IL を生成
>ildasm /out:commonlib.il commonlib.dll
IL を修正

ildasm で生成された IL(ここでは commonlib.il ファイル)内の以下の箇所を

string [AspectDngDynamicClassName]AspectDngDynamicClassName1::name

次のように置き換えて IL ファイルを保存する。

string Data::name
ilasm でアセンブリを再生成

変更した IL ファイルを使ってアセンブリを再生成する

>ilasm /DLL commonlib.il