C#
Autofac

Autofac について調べてみた その1 インスタンスのスコープ

More than 1 year has passed since last update.

C# のプロジェクトに参加することになりそうなので、C# の DI の仕組みである Autofac について調べてみた。今回は、Autofac のスコープについて調べてみた。全てのサンプルコードはAutfacSampleに置いておいた。

Autofac とは

Autofac は .NET 用の IoC コンテナだ。アプリケーションに対して簡単に、Dependency Injection が可能になる。最初の例から見てみよう。

最初のサンプル

Backend.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AutoSample
{
    public interface IBackend
    {
        string getContents(string message);
        Guid InstanceID();
    }
    class Backend : IBackend
    {
        private Guid InstanceID;
        public Backend()
        {
            this.InstanceID = Guid.NewGuid();
        }

        public string getContents(string message)
        {
            return $"[Backend]: {message}";
        }

        Guid IBackend.InstanceID()
        {
            return this.InstanceID;
        }
    }
}

SingleSample.cs

using Autofac;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AutoSample
{
    public class SingleSample
    {
        private static IContainer Container { get; set; }
        public void Exec()
        {
            Console.WriteLine("SingleInstance ----");
            var builder = new ContainerBuilder();
            builder.RegisterType<Backend>().As<IBackend>().SingleInstance();
            Container = builder.Build();
            GetData();
        }
        private static void GetData()
        {
            using (var scope = Container.BeginLifetimeScope())
            {
                var writer = scope.Resolve<IBackend>();
                var greeting = "hi";
                var serverMessage = writer.getContents("hi");
                Console.WriteLine($"sent {greeting} response {serverMessage} ");

                var writer2 = scope.Resolve<IBackend>();
                Console.WriteLine($"write 01 {writer.InstanceID()} write 02 {writer2.InstanceID()}");

            }
        }

    }
}

Program.cs

using Autofac;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AutoSample
{
    class Program
    {
        private static IContainer Container { get; set; }
        static void Main(string[] args)
        {
            new SingleSample().Exec();
            Console.ReadLine();

        }

    }
}

ポイントになる部分について解説していこう。

ビルダーへの登録

DI(Dependency Injection) を実施したいクラスをContainer Builder に登録していく。ここでは、Backend クラスを、IBackend インターフェイスの実体として登録している。そして、スコープとして、SingeInstance つまり、何回 IBackend の実体を取得しようとしても、同じインスタンスが返される。Singleton パターンのようにふるまってくれる。

            var builder = new ContainerBuilder();
            builder.RegisterType<Backend>().As<IBackend>().SingleInstance();
            Container = builder.Build();

ちなみに、最初のコードでは、Backend クラスのインスタンスを、IBackend の実体として登録しているだけだが、オブジェクトを組み合わせて返却するということも実施してくれる。後ほどサンプルで出てくるのでお楽しみに。

インスタンスの取得

ここで、登録したクラスを使いたいときは次のようなコードで実行する。ビルダに登録したクラスを取得するときには下記の通り。インターフェイスをしか、参照していないので、ビルダを切り替えると、簡単に処理をMockに切り替えたり、別のインターフェイスの実装クラスを参照させることができる。

            using (var scope = Container.BeginLifetimeScope())
            {
                var writer = scope.Resolve<IBackend>();
                var greeting = "hi";
                var serverMessage = writer.getContents("hi");
             }

上記のプログラムを実行してみるSingleSample の Exec() メソッドでは、 Backend を2回取得しているが、実行結果で分かる通り、インスタンス番号は同じになっている。

SingleInstance ----
sent hi response [Backend]: hi
write 01 24651457-06a1-4910-a6f0-6b972c7250c7 write 02 24651457-06a1-4910-a6f0-6b972c7250c7

SingleInstance() のスコープ

これを少しだけ変更してみよう。

SingleSample の Exec() メソッドを変えてみる。

もともと

            builder.RegisterType<Backend>().As<IBackend>().SingleInstance();

だったのを

            builder.RegisterType<Backend>().As<IBackend>();

に変えて実行する。すると、シングルインスタンスが保証されなくなるので、すると、違う Backend のインスタンスが使われていることがわかる。

SingleInstance ----
sent hi response [Backend]: hi
write 01 26f37882-e0ed-45e9-a4e4-db43fdcdcc42 write 02 7b7d9532-ec40-477b-973c-4d081e49ee42

Instance Per Lifetime のスコープ

次に、インスタンス毎のインスタンススコープを試してみる。

InstancePerLifetimeSample.cs

using Autofac;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AutoSample
{
    public class InstancePerLifetimeSample
    {
        private static IContainer Container { get; set; }
        public void Exec()
        {
            Console.WriteLine("Instance Per Lifetime Scope ----");
            var builder = new ContainerBuilder();
            builder.RegisterType<Backend>().As<IBackend>().InstancePerLifetimeScope();
            Container = builder.Build();
            GetData();
        }
        private static void GetData()
        {
            using (var scope1 = Container.BeginLifetimeScope())
            {
                var writer = scope1.Resolve<IBackend>();
                var greeting = "hi";
                var serverMessage = writer.getContents(greeting);
                Console.WriteLine($"sent {greeting} response {serverMessage} ");

                var writer2 = scope1.Resolve<IBackend>();
                Console.WriteLine($"write 01 {writer.InstanceID()} write 02 {writer2.InstanceID()}");

            }
            using (var scope2 = Container.BeginLifetimeScope())
            {
                var writer = scope2.Resolve<IBackend>();
                var greeting = "hello";
                var serverMessage = writer.getContents(greeting);
                Console.WriteLine($"sent {greeting} response {serverMessage} ");

                var writer2 = scope2.Resolve<IBackend>();
                Console.WriteLine($"write 03 {writer2.InstanceID()} write 04 {writer2.InstanceID()}");

                using (var scope3 = scope2.BeginLifetimeScope())
                {
                    var writer3 = scope3.Resolve<IBackend>();
                    Console.WriteLine($"write 05 {writer3.InstanceID()}");
                }

            }
        }
    }
}

ポイントとしては、次のように、インスタンス毎のインスタンススコープを指定している。

builder.RegisterType<Backend>().As<IBackend>().InstancePerLifetimeScope();

その後、scope 1 として、IBackend を2回取得しており、scope 2 として、2回。そして、scope2 の子スコープとして、scope3 で1回取得している。実行してみよう。

Instance Per Lifetime Scope ----
sent hi response [Backend]: hi
write 01 a92b5b8d-1a5a-4039-bec8-c9448fb95f1f write 02 a92b5b8d-1a5a-4039-bec8-c9448fb95f1f
sent hello response [Backend]: hello
write 03 1b6e332f-002d-4a63-a096-af74eb06765f write 04 1b6e332f-002d-4a63-a096-af74eb06765f
write 05 2c99b08b-c130-412e-9888-685e36801762

スコープの内部で、取得した IBackend が同一のインスタンスになっている。スコープが変わると変わるようだ。ちなみに、最後の write 05 は、scope2 の子として、scope 3を作成しているが、scope 2 とは違うインスタンスになっている。

Instance Per Matching Lifetime のスコープ

先ほどの例で、scope 2 と、 scope 3 は scope 2 の内部なので同じインスタンスを返してほしい場合はどうすればいいだろうか?その場合は、Instance Per Matching Lifetime のスコープを使うとよい。

builder.RegisterType<Backend>().As<IBackend>().InstancePerMatchingLifetimeScope("aRequest");

GetData() の中身を変更してみた。Container.BeginLifetimeScope("aRquest") といったように、名前を指定してスコープを始めている。

            using (var scope1 = Container.BeginLifetimeScope("aRequest"))
            {
                var writer = scope1.Resolve<IBackend>();
                var greeting = "hi";
                var serverMessage = writer.getContents(greeting);
                Console.WriteLine($"sent {greeting} response {serverMessage} ");

                var writer2 = scope1.Resolve<IBackend>();
                Console.WriteLine($"write 01 {writer.InstanceID()} write 02 {writer2.InstanceID()}");

            }
            using (var scope2 = Container.BeginLifetimeScope("aRequest"))
            {
                var writer = scope2.Resolve<IBackend>();
                var greeting = "hello";
                var serverMessage = writer.getContents(greeting);
                Console.WriteLine($"sent {greeting} response {serverMessage} ");
                using (var scope3 = scope2.BeginLifetimeScope()) // scope 2 の子として、scope 3 を作成
                {

                    var writer2 = scope3.Resolve<IBackend>();  // scope 3 からインスタンスを取得
                    Console.WriteLine($"write 03 {writer2.InstanceID()} write 04 {writer2.InstanceID()}"); // このインスタンスは、scope2のインスタンスと同じ
                }
            }

このようにすると、scope2 の子のスコープであるscope3 でも、同じインスタンスが使われているのがわかる。

Instance Per Matching Lifetime Scope ----
sent hi response [Backend]: hi
write 01 9d381939-777e-4462-a708-aa5d2ccd5e40 write 02 9d381939-777e-4462-a708-aa5d2ccd5e40
sent hello response [Backend]: hello
write 03 a14d4897-5382-455e-9f6a-257cf8a532b4 write 04 a14d4897-5382-455e-9f6a-257cf8a532b4

ちなみに、同じものを2回連続で、名前を変えて登録すると、より下で定義したほうのみが有効になる様子。この場合、bRequestのみが有効になり、aRequestで参照するとエラーになる。

builder.RegisterType<Backend>().As<IBackend>().InstancePerMatchingLifetimeScope("aRequest");
builder.RegisterType<Backend>().As<IBackend>().InstancePerMatchingLifetimeScope("bRequest");

Instance Per Owned スコープ

Autofac で管理しているインスタンスで、Command が IBackend を持っているようなケースで、IBackend のスコープを、Command の生存範囲に合わせたいという場合に使われる。

using Autofac;
using Autofac.Features.OwnedInstances;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AutoSample
{
    public class InstancePerOwnedSampleSample
    {
        private static IContainer Container { get; set; }
        public void Exec()
        {
            Console.WriteLine("Instance Per Owned Scope ----");
            var builder = new ContainerBuilder();
            builder.RegisterType<Command>();
            builder.RegisterType<Backend>().As<IBackend>().InstancePerOwned<Command>(); // Instance PerOwned を指定
            Container = builder.Build();
            Console.WriteLine("command 01 ---");
            using (var scope1 = Container.BeginLifetimeScope())
            {
                var command1 = scope1.Resolve<Owned<Command>>(); // インスタンスは、Owned にラップされて返却される
                command1.Value.Exec(); 
                command1.Value.Exec();
                command1.Dispose(); // command1 を明示的に削除している
                var command2 = scope1.Resolve<Owned<Command>>();
                command2.Value.Exec();
                command2.Value.Exec();
                command2.Dispose();
            }

            Console.WriteLine("command 02 ---");
            using (var scope2 = Container.BeginLifetimeScope())
            {
                var command2 = scope2.Resolve<Owned<Command>>();
                command2.Value.Exec();
                command2.Value.Exec();
            }

        }


        public class Command 
        {
            private IBackend backend;

            public Command(IBackend backend) // Command に IBackend を Injection する書き方
            {
                this.backend = backend;
            }
           public void Exec()
            { 
                Console.WriteLine($"Command: Backend is executed InstanceID: {backend.InstanceID()}");
            }
        }

    }
}

ちなみに、このスコープの場合、オブジェクトの取得のやり方が少し変わる。

var builder = new ContainerBuilder();
builder.RegisterType<Command>();
builder.RegisterType<Backend>().As<IBackend>().InstancePerOwned<Command>(); // Instance PerOwned を指定
Container = builder.Build();

ここでは、Command と、IBackend を登録しており、IBackend を、Command 毎に作成するように指定している。ちなみに、Command のコンストラクタは次のようになっている。特に指定していないが、IBackend Inject されて、インスタンスが生成される。

public class Command 
{
    private IBackend backend;

    public Command(IBackend backend) // Command に IBackend を Injection する書き方
    {
       this.backend = backend;
    }
:

取得するときも、一工夫必要で

harp
var command1 = scope1.Resolve<Owned<Command>>(); // インスタンスは、Owned にラップされて返却される
command1.Value.Exec(); 
command1.Value.Exec();
command1.Dispose(); // command1 を明示的に削除している

インスタンスは、Owned オブジェクトにラップして返却されるので、Command のメソッドを使いたければ、Value プロパティ経由になる。また、Command のインスタンスのライフタイムを明確にコントロールするため Command の使用が終わったら、Dispose() メソッドを呼ぶ。

たしかに、Command の生存範囲と、Backend のインスタンスの生存スコープは同じになっている。

Instance Per Owned Scope ----
command 01 ---
Command: Backend is executed InstanceID: 9398e561-1939-470f-b3c1-acc9023be3d8
Command: Backend is executed InstanceID: 9398e561-1939-470f-b3c1-acc9023be3d8
Command: Backend is executed InstanceID: 38cdfff0-fbf9-4f35-8572-c58f54b25319
Command: Backend is executed InstanceID: 38cdfff0-fbf9-4f35-8572-c58f54b25319
command 02 ---
Command: Backend is executed InstanceID: 9e5a7024-ec89-4a99-8d35-087258f61cf7
Command: Backend is executed InstanceID: 9e5a7024-ec89-4a99-8d35-087258f61cf7

Instance Per Owned 未解決事項

ちなみに、

builder.RegisterType<Backend>().As<IBackend>().InstancePerOwned<Command>(); // Instance PerOwned を指定

builder.RegisterType<Backend>().As<IBackend>();

に変更して、

            using (var scope1 = Container.BeginLifetimeScope())
            {
                var command1 = scope1.Resolve<Owned<Command>>(); // インスタンスは、Owned にラップされて返却される
                command1.Value.Exec(); 
                command1.Value.Exec();
                command1.Dispose(); // command1 を明示的に削除している
                var command2 = scope1.Resolve<Owned<Command>>();
                command2.Value.Exec();
                command2.Value.Exec();
                command2.Dispose();
            }

            Console.WriteLine("command 02 ---");
            using (var scope2 = Container.BeginLifetimeScope())
            {
                var command2 = scope2.Resolve<Owned<Command>>();
                command2.Value.Exec();
                command2.Value.Exec();
            }

        }
            using (var scope1 = Container.BeginLifetimeScope())
            {
                var command1 = scope1.Resolve<Command>(); 
                command1.Exec(); 
                command1.Exec();
                var command2 = scope1.Resolve<Command>();
                command2.Exec();
                command2.Exec();
            }

            Console.WriteLine("command 02 ---");
            using (var scope2 = Container.BeginLifetimeScope())
            {
                var command2 = scope2.Resolve<Command>();
                command2.Exec();
                command2.Exec();
            }

        }

にしても、結果は変わらない。じゃあ何がいいかというと、command の生存期間を、Dispose で明確化できることらしい。がどういうケースでそれが有効なのかがよくわかっていない。もしご存知の方がおられたら是非コメントいただきたい。

Thread スコープ

Thread 毎に IBackend が生成されてほしいというケースでは、Instance Per Lifetime のスコープを使うと、想定通りの動作を実現することができる。スレッド毎に、スコープを生成すればよい。
コメントにご注目いただきたい。

using Autofac;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace AutoSample
{
    public class ThreadScopeSample
    {
        private static IContainer Container { get; set; }
        public void Exec()
        {
            Console.WriteLine("Thread Scope ----");
            var builder = new ContainerBuilder();
            builder.RegisterType<ThreadCreator>();
            builder.RegisterType<Backend>().As<IBackend>().InstancePerLifetimeScope(); // Instance Per Lifetime を指定
            Container = builder.Build();
            using (var scope = Container.BeginLifetimeScope())
            {
                var tc = scope.Resolve<ThreadCreator>();
                Thread aThread = new Thread(new ThreadStart(tc.ThreadExec));
                Thread bThread = new Thread(new ThreadStart(tc.ThreadExec));
                aThread.Start();
                bThread.Start();
                aThread.Join();
                bThread.Join();
            }
        }
    }

    public class ThreadCreator
    {
        private ILifetimeScope parentScope;

        public ThreadCreator(ILifetimeScope scope) // 親スコープが死ぬとおかしな挙動になるので、GCに合わないように。
        {
            parentScope = scope;
        }
        public void ThreadExec()
        {
            using (var scope = parentScope.BeginLifetimeScope()) // スレッド毎にスコープが作られる
            {
                var backend1 = scope.Resolve<IBackend>();
                var backend2 = scope.Resolve<IBackend>();
                Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId} backend1 : {backend1.InstanceID() } backend2 : {backend2.InstanceID() }");
            }
        }
    }


}

実行結果

Thread Scope ----
Thread: 4 backend1 : c85d5f5c-f026-47b8-8169-9c586f8482ef backend2 : c85d5f5c-f026-47b8-8169-9c586f8482ef
Thread: 3 backend1 : 56adde87-d834-4a70-902b-645d0eea2824 backend2 : 56adde87-d834-4a70-902b-645d0eea2824

スレッド毎にきれいにインスタンスが分かれている。

次は、Owin や、Api への DI の組み込みについて見ていきたい。近い将来 Moq を調べて、Test Drive Development を、 C# で常時可能なようにしていきたい。