LoginSignup
15
29

More than 5 years have passed since last update.

C#でアスペクト指向

Last updated at Posted at 2016-11-26

※個人的な備忘録とし記述します。が、一応他の人にも見られてもいいように記述する予定です。
下図のような処理を実行するプログラムを作成します。

Aspect01.png

サンプルプログラム

以下のサンプルアプリケーションを実行した結果「SampleClass.SampleMethod」メソッドを実行前後にログを出力(今回はコンソール出力ですが)することが最終目標です。

アプリ実装は「MyAspect」属性をつけ、「ContextBoundObject」クラスを継承する必要がありますが、それ以外に特別なことをする必要はありません。

Sampleコード1

    //EntryPoint
    class Program
    {
        static void Main(string[] args)
        {
            SampleClass cls = new SampleClass();
            cls.SampleMethod("テスト実行");
            Console.ReadLine();
        }
    }

    //実行対象クラス
    [MyAspect]
    public class SampleClass : ContextBoundObject
    {
        public string SampleMethod(string str)
        {
            DateTime now = DateTime.Now;
            return null;
        }
    }
実行結果
[2016/11/27 14:27:54]SampleMethod : 実行開始
[2016/11/27 14:27:54]SampleMethod : 実行終了

アスペクトの実装

「MyAspect」属性クラスは「ProxyAttribute」クラスを継承します。この属性はProxyを使いますよ~ってことを表す属性です。Proxyとは「代理」の意味です。上記「SampleClass」クラスのメソッドを、後述のProxyクラスが代理で実行しますって考えていればよいかと。
「MyAspect」属性クラスの実装は以下の通り

MyAspectAttributeクラス
    public class MyAspectAttribute : ProxyAttribute
    {
        public override MarshalByRefObject CreateInstance(Type serverType)
        {
            MarshalByRefObject target = base.CreateInstance(serverType);
            RealProxy rp;
            rp = new MyProxy(target, serverType);
            return rp.GetTransparentProxy() as MarshalByRefObject;
        }
    }

「MyAspect」属性クラスの「CreateInstance」メソッド内で「MyProxy」クラスをNewしています。ここでNewしている「MyProxy」クラスが対象のMethodを実行します。「MyProxy」クラスが「SampleClass」クラスのメソッドの実行を「代理」しているのです。ということは、「SampleClass」クラスのメソッドの実行している処理の前後にログ出力処理を実装すればよいのです。

MyProxyクラス
    public class MyProxy : RealProxy
    {
        private MarshalByRefObject _target;

        public MyProxy(MarshalByRefObject target, Type t) : base(t)
        {
            this._target = target;
        }

        public override IMessage Invoke(IMessage msg)
        {
            IMethodCallMessage call = (IMethodCallMessage)msg;
            IMethodReturnMessage res;
            IConstructionCallMessage ctor = call as IConstructionCallMessage;

            if (ctor != null)
            {
                //以下、コンストラクタを実行する処理

                RealProxy rp = RemotingServices.GetRealProxy(this._target);
                res = rp.InitializeServerObject(ctor);
                MarshalByRefObject tp = this.GetTransparentProxy() as MarshalByRefObject;
                res = EnterpriseServicesHelper.CreateConstructionReturnMessage(ctor, tp);
            }
            else
            {
                //以下、コンストラクタ以外のメソッドを実行する処理

                //メソッド前処理
                Console.WriteLine("[{0}]{1} : 実行開始", 
                    DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), call.MethodName);

                //メソッド実行
                res = RemotingServices.ExecuteMessage(this._target, call);

                //メソッド後処理
                Console.WriteLine("[{0}]{1} : 実行終了",
                    DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), call.MethodName);
            }

            return res;
        }
    }

上記のサンプルプログラムでは単純なログ出力だけですが、呼び出したメソッドの引数や戻り値、発生したException等も取得できます。また今回のログ出力のような処理をManaged Extensibility Framework (MEF) を使って拡張できるようにすることも可能です。

Managed Extensibility Framework (MEF) で機能拡張

自分的にアスペクトとMEFは相性がいいと思っています。(性能的にどうなの?ってのはありますが。)
そんなわけで、MEFを使って拡張していきます。イメージ的には下記の感じです。

Aspect02.png

まずはアスペクトから呼び出すPartsのインターフェースを定義します。インターフェースには実行対象のメソッドが呼び出し前に実行されるメソッド「PreProcessing」と呼び出し後に実行されるメソッド「PostProcessing」を定義します。

IMyPartsインターフェース
    public interface IMyParts
    {
        // メソッド実行前処理メソッド
        void PreProcessing(IMethodCallMessage call);

        // メソッド実行後処理メソッド
        void PostProcessing(IMethodCallMessage call, IMethodReturnMessage res);
    }

「MyCompositionContainer」クラスは以下のような感じ。
コンストラクタでカレントディレクトリのDLLファイル内のPartsクラスを検索し、「CompositionContainer」に登録。「PreProcessing」メソッド「PostProcessing」メソッドを呼び出した時に、「CompositionContainer」に登録されているクラスの「PreProcessing」メソッド「PostProcessing」メソッドを呼び出します。
※各メソッド内の処理については今後追記したいと思います。追記されるまではGoogleさんやBingさんで検索でもしてください。

MyCompositionContainerクラス
    public class MyCompositionContainer : IDisposable
    {
        private CompositionContainer _container = null;

        public void Dispose()
        {
            _container.Dispose();
        }

        public MyCompositionContainer()
        {
            string currentDir = Directory.GetCurrentDirectory();

            var aggCatalog = new AggregateCatalog();
            var builder = new RegistrationBuilder();
            builder.ForTypesDerivedFrom<IMyParts>().ExportInterfaces();
            foreach (var f in new DirectoryInfo(currentDir).GetFiles().
                        Where(x => x.Name.ToLower().EndsWith(".dll")))
            {
                var catalog = new AssemblyCatalog(Assembly.LoadFile(f.FullName), builder);
                aggCatalog.Catalogs.Add(catalog);
            }
            this._container = new CompositionContainer(aggCatalog);
        }

        internal void PreProcessing(IMethodCallMessage call)
        {
            IEnumerable<IMyParts> parts = _container.GetExportedValues<IMyParts>();
            foreach (var item in parts)
            {
                item.PreProcessing(call);
            }
        }

        internal void PostProcessing(IMethodCallMessage call, IMethodReturnMessage res)
        {
            IEnumerable<IMyParts> parts = _container.GetExportedValues<IMyParts>();
            foreach (var item in parts)
            {
                item.PostProcessing(call, res);
            }
        }
    }

次はPartsクラスの実装です。「IMyParts」インターフェースを実装ます。「IMyParts」インターフェースを実装することで、アスペクトが動的に読み込みます(カレントディレクトリにアセンブリを配置する必要がありますが)。
で、上記の「MyProxy」クラスで実装したログ出力処理をPartsクラスに記述します。

MyLogPartsクラス
    public class MyLogParts : IMyParts
    {
        public void PreProcessing(IMethodCallMessage call)
        {
            //メソッド前処理
            Console.WriteLine("[{0}]{1} : 実行開始",
                DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), call.MethodName);
        }

        public void PostProcessing(IMethodCallMessage call, IMethodReturnMessage res)
        {
            //メソッド後処理
            Console.WriteLine("[{0}]{1} : 実行終了",
                DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), call.MethodName);
        }
    }

最後に「MyProxy」クラスをMEF版に変更します。「MyCompositionContainer」クラスをコンストラクタでNewし、実行対象メソッドを呼び出す前後に「MyCompositionContainer」クラスの「PreProcessing」メソッド「PostProcessing」メソッドを呼び出すだけです。

MyProxyクラス(MEF版)
    public class MyProxy : RealProxy
    {
        private MarshalByRefObject _target;

        private MyCompositionContainer _container = null;

        public MyProxy(MarshalByRefObject target, Type t) : base(t)
        {
            this._target = target;
            this._container = new MyCompositionContainer();
        }

        public override IMessage Invoke(IMessage msg)
        {
            IMethodCallMessage call = (IMethodCallMessage)msg;
            IMethodReturnMessage res;
            IConstructionCallMessage ctor = call as IConstructionCallMessage;

            if (ctor != null)
            {
                //以下、コンストラクタを実行する処理

                RealProxy rp = RemotingServices.GetRealProxy(this._target);
                res = rp.InitializeServerObject(ctor);
                MarshalByRefObject tp = this.GetTransparentProxy() as MarshalByRefObject;
                res = EnterpriseServicesHelper.CreateConstructionReturnMessage(ctor, tp);
            }
            else
            {
                //以下、コンストラクタ以外のメソッドを実行する処理

                //メソッド前処理
                //Console.WriteLine("[{0}]{1} : 実行開始", 
                //    DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), call.MethodName);
                this._container.PreProcessing(call);

                //メソッド実行
                res = RemotingServices.ExecuteMessage(this._target, call);

                //メソッド後処理
                //Console.WriteLine("[{0}]{1} : 実行終了",
                //    DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), call.MethodName);
                this._container.PostProcessing(call, res);
            }

            return res;
        }
    }

変更履歴

# 更新日 内容
1 2016/11/26 とりあえず追加
2 2016/11/27 コードを追記
3 2016/11/27 微修正
4 2016/12/18 MEF機能を追記
15
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
29