taediumの日記

2010-03-18

[][] コンポーネント登録をせずにDIしてインターセプタを適用

自動登録という機能があるわけではなく、Kernel(コンテナ)に問い合わせて存在しなかったらインスタンス化したりDIしてくれるようです。これは便利。

それと、IKernelのIntercept拡張メソッドを使うと条件に引っかかるメソッドにインターセプタが適用できます。下のコードでは全メソッドを対象にしています。

    public class NinjectTest
    {
        [Fact]
        public void TestIntercept()
        {
            var kernel = new StandardKernel(new NinjectSettings{ LoadExtensions = false}, new DynamicProxy2Module());
            kernel.Intercept(proxy => true).With<TraceInterceptor>();

            var foo = kernel.Get<Foo>(); // Fooを登録せずにいきなりGet
            foo.DoFoo();
        }
    }
    public class Foo
    {
        [Inject]
        public Bar Bar { get; set;}

        public virtual void DoFoo()
        {
            Console.WriteLine("DoFoo invoked");
            Bar.DoBar();
        }
    }
    public class Bar
    {
        public virtual void DoBar()
        {
            Console.WriteLine("DoBar invoked.");
        } 
    }
    public class TraceInterceptor : SimpleInterceptor
    {
        protected override void BeforeInvoke(IInvocation invocation)
        {
            Console.WriteLine("Before {0}", invocation.Request.Method.Name);
        }
        protected override void AfterInvoke(IInvocation invocation)
        {
            Console.WriteLine("After {0}", invocation.Request.Method.Name);
        }
    }

実行結果はこうなります。

Before DoFoo
DoFoo invoked
Before DoBar
DoBar invoked.
After DoBar
After DoFoo

FooにBarがDIされていますし、両方に対してインターセプタがかかっています。

[][] 登録していないコンポーネントが自動でインスタンス化されるのを防ぐには

上で示した挙動がいやな場合もあるかもしれません。

そのときは、IKernelの実装クラスを作成してそれを使います。具体的にはIKernelのHandleMissingBindingメソッドをオーバーライドしてfalseを返します。

    public class MissingBindingDisabledKernel : StandardKernel
    {

        public MissingBindingDisabledKernel(INinjectSettings settings, params INinjectModule[] modules) : base(settings, modules) { }

        protected override bool HandleMissingBinding(Type service)
        {
            return false;
        }
    }

こうすると、登録していないコンポーネントをKernelから取得しようとした時点で例外が発生します。

2010-03-11

[][] NinjectでInterceptorをつかう

Inteceptorの機能はExtensionとして提供されています。ダウンロードはここからできます。

インターセプタの実装が2つ(Castle DynamicProxyとLinFu)あるのでどちらか版を選ぶといいと思います。軽く調べた限りではCastleの方が信頼性があるっぽいので、Interception 2.0.0.0 .NET 3.5 Binaries with Castle DynamicProxy 2.2を使うことにします。

拡張メソッドを使ってNinjectのCore部分を拡張しているので、適切にusingしておかないと補完がききません。とりあえずこの3つを。

using Ninject;
using Ninject.Extensions.Interception;
using Ninject.Extensions.Interception.Infrastructure.Language;

インターセプトの対象。

public class Person
{
    public virtual void SayHello(string name)
    {
        Console.WriteLine("Hello {0}", name);
    }
}

SimpleInterceptorを継承すると簡単に作れます。ここではメソッドの呼び出し前後に適当にコンソール出力します。

public class TraceInterceptor : SimpleInterceptor
{
    protected override void BeforeInvoke(IInvocation invocation)
    {
        Console.WriteLine("Before");
    }
    protected override void AfterInvoke(IInvocation invocation)
    {
        Console.WriteLine("After");
    }
}

テストメソッド。LoadExtensions = falseにしておかないと勝手にモジュール読み込んでエラーになってしまうですよね。インターセプタのモジュールはDynamicProxy2Moduleを使います。

[TestMethod()]
public void InterceptorTest()
{
    var kernel = new StandardKernel(new NinjectSettings { LoadExtensions = false}, new DynamicProxy2Module());
    kernel.Bind<Person>().ToSelf().Intercept().With(new TraceInterceptor());
    var p = kernel.Get<Person>();
    p.SayHello("aaa");
}

実行結果

Before
Hello aaa
After

ここで、一番重要なのは、インターセプトの対象メソッドをvirtualにすること。こうしとかないとインターセプトされません。これに気づかなくてそーとう長いことはまりました。


作者のブログはここです。ドキュメントないよってコメントしたからか最近のエントリで使い方書いてくれました。

[][] 属性でインターセプトの対象を示す

usingしておきます。

using Ninject;
using Ninject.Extensions.Interception;
using Ninject.Extensions.Interception.Attributes;
using Ninject.Extensions.Interception.Infrastructure.Language;
using Ninject.Extensions.Interception.Request;        

インターセプター。

public class TraceInterceptor : SimpleInterceptor
{
    protected override void BeforeInvoke(IInvocation invocation)
    {
        Console.WriteLine("Before");
    }
    protected override void AfterInvoke(IInvocation invocation)
    {
        Console.WriteLine("After");
    }
}

属性はInterceptAttributeを継承して作ります。

public class TraceAttribute : InterceptAttribute
{
    public override IInterceptor CreateInterceptor(IProxyRequest request)
    {
        return request.Context.Kernel.Get<TraceInterceptor>();
    }
}

属性をクラスにつけます。メソッドにもつけられます。

[Trace]
public class Person 
{            
    public virtual void SayHello(string name)
    {
        Console.WriteLine("Hello {0}", name);
    }

    public virtual void SayGoodbye(string name)
    {
        Console.WriteLine("Good bye {0}", name);
    }
}

セットアップと実行。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void TraceAttributeTest()
{
    var kernel = new StandardKernel(new NinjectSettings { LoadExtensions = false}, new DynamicProxy2Module());
    kernel.Bind<Person>().ToSelf();
    var p = kernel.Get<Person>();
    p.SayHello("aaa");
    p.SayGoodbye("bbb");
}

実行結果。

Before
Hello aaa
After
Before
Good bye bbb
After

Personのメソッド実行時にインターセプタが動いていることが確認できます。

2010-03-09

[][] IntelliSenseに表示させないことで流れるインタフェースをより滑らかに

Ninjectのコードを読んでいて知りました。そういうことができるんですね。

	/// <summary>
	/// A hack to hide methods defined on <see cref="System.Object"/> for IntelliSense
	/// on fluent interfaces. Credit to Daniel Cazzulino.
	/// </summary>
	[EditorBrowsable(EditorBrowsableState.Never)]
	public interface IFluentSyntax
	{
		/// <inheritdoc/>
		[EditorBrowsable(EditorBrowsableState.Never)] Type GetType();

		/// <inheritdoc/>
		[EditorBrowsable(EditorBrowsableState.Never)] int GetHashCode();

		/// <inheritdoc/>
		[EditorBrowsable(EditorBrowsableState.Never)] string ToString();

		/// <inheritdoc/>
		[EditorBrowsable(EditorBrowsableState.Never)] bool Equals(object other);
	}

別のアセンブリにしないと効かないみたいです。

2010-03-07

[][] Ninjectを使ってみた

C#で書かれた軽量なDIコンテナNinjectをちょっとだけ使ってみました。GuiceC#版のようです。

最近バージョン2がリリースされたみたいですね。

サイト

作者のブログ

あんまりドキュメントがないのが残念ですが、コードはきれいでコンパクトにまとまっている印象です。

例にのっていたサンプルプログラムを適当に変更して実行してみました。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void First_step()
{
    var kernel = new StandardKernel(new WarriorModule());
    var samurai = kernel.Get<Samurai>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

public interface IWeapon
{
    string Hit(string target);
}

public class Sword : IWeapon
{
    public string Hit(string target)
    {
        return string.Format("{0}を切りつけた", target);
    }
}

public class Samurai
{
    private IWeapon _weapon;

    [Inject]
    public Samurai(IWeapon weapon) {
        _weapon = weapon;
    }

    public string Attack(string target)
    {
        return _weapon.Hit(target);
    }
}

public class WarriorModule : NinjectModule
{
    public override void Load()
    {
        Bind<IWeapon>().To<Sword>();
        Bind<Samurai>().ToSelf();
    }
}

Moduleは必須ではないみたい。Kernelに直接Bindできます。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Bind_without_module()
{
    var kernel = new StandardKernel();
    kernel.Bind<IWeapon>().To<Sword>();
    kernel.Bind<Samurai>().ToSelf();
    var samurai = kernel.Get<Samurai>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

インスタンスのスコープはデフォルトだとtransientだそうです。つまり毎回つくられます。Javaだとprototypeとよんだりしますが。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Bind_in_default_scope()
{
    var kernel = new StandardKernel();
    kernel.Bind<IWeapon>().To<Sword>();
    kernel.Bind<Samurai>().ToSelf();
    Assert.AreNotSame(kernel.Get<Samurai>(), kernel.Get<Samurai>());
}

singletonにするには、InSingletonScopeメソッドを呼びます。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Bind_in_singleton_scope()
{
    var kernel = new StandardKernel();
    kernel.Bind<IWeapon>().To<Sword>();
    kernel.Bind<Samurai>().ToSelf().InSingletonScope();
    Assert.AreSame(kernel.Get<Samurai>(), kernel.Get<Samurai>());
}

[][] Ninjectを使ってみた その2

モジュールを動的に読み込めます。まず、Assemblyから。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Dynamic_module_loading_by_Assembly()
{
    var kernel = new StandardKernel();
    kernel.Load(AppDomain.CurrentDomain.GetAssemblies().Where(e => e.FullName.StartsWith("Client"))); // <-- ここ
    var samurai = kernel.Get<Samurai>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

次に、カレントディレクトリ上に存在するdllから。Ninjectの拡張ライブラリを使うか自分でコンポーネントつくるかすればrubyxmlなどで記述した設定を読めたりすることもできそう。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Dynamic_module_loading_by_path()
{
    var kernel = new StandardKernel();
    kernel.Load("Client.dll"); // <-- ここ
    var samurai = kernel.Get<Samurai>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

Provier経由でインスタンス化できます。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Providers()
{
    var kernel = new StandardKernel();
    kernel.Bind<IWeapon>().ToProvider<SwordProvider>(); // <-- ここ
    kernel.Bind<SamuraiM>().ToSelf();
    var samurai = kernel.Get<Samurai>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

public class SwordProvider : Provider<Sword>
{
    protected override Sword CreateInstance(IContext context)
    {
        return new Sword();
    }
}

delegateを使ってインスタンス化することもできます。上のProvider使う方法を簡易化したかんじですね。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void FactoryMethods()
{
    var kernel = new StandardKernel();
    kernel.Bind<IWeapon>().ToMethod(context => new Sword()); // <-- ここ
    kernel.Bind<SamuraiM>().ToSelf();
    var samurai = kernel.Get<Samurai>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

[][] Ninjectを使ってみた その3

インジェクションのタイプは、3つだそうです。

  • コンストラクタインジェクション
  • プロパティインジェクション
  • メソッドインジェクション

バージョン1のころはフィールドインジェクションがあったようですが、バージョン2で廃止したそうです。C#VB.NETの場合はプロパティインジェクションがあれば不要ということでしょうね。


プロパティインジェクション。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Property_injection()
{
    var kernel = new StandardKernel();
    kernel.Bind<IWeapon>().To<Sword>();
    kernel.Bind<SamuraiP>().ToSelf();
    var samurai = kernel.Get<SamuraiP>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

public class SamuraiP
{
    [Inject]
    public IWeapon _weapon {private get;set;}

    public string Attack(string target)
    {
        return _weapon.Hit(target);
    }
}

メソッドインジェクション。

[global::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
public void Method_injection()
{
    var kernel = new StandardKernel();
    kernel.Bind<IWeapon>().To<Sword>();
    kernel.Bind<SamuraiM>().ToSelf();
    var samurai = kernel.Get<SamuraiM>();
    Assert.AreEqual(samurai.Attack("忍者"), "忍者を切りつけた");
}

public class SamuraiM
{
    private IWeapon _weapon;

    [Inject]
    public void Arm(IWeapon weapon)
    {
        _weapon = weapon;
    }

    public string Attack(string target)
    {
        return _weapon.Hit(target);
    }
}