インフィニットループ 技術ブログ

2022年12月27日 (火)

著者 : nagodon

PHP開発者がC#でDIを使った時のハマリポイント

こんにちは、バックエンドの開発を担当しているなごちん、こと名古屋です。

札幌はすっかり雪化粧になってしまいまして
例年初雪は一度溶けてしまう事が多いんですが今年は一回も溶けずに年を迎えそうです。

そんな今日はPHP開発者がC#(.NET)で実装した時のDIのハマりポイントについて書きたいと思います。
.NETのバージョンは5でASP.NET Coreでのお話になります。

DIとは

依存性の注入の事を指し、Wikipediaでは以下の形で説明されています。

依存性の注入(いぞんせいのちゅうにゅう、英: Dependency injection)とは、あるオブジェクトや関数が、依存する他のオブジェクトや関数を受け取るデザインパターンである。英語の頭文字からDIと略される。DIは制御の反転の一種で、オブジェクトの作成と利用について関心の分離を行い、疎結合なプログラムを実現することを目的としている。

依存性の注入

注入方法としてはコンストラクタインジェクションやセッターインジェクション等があり、対象のクラスをインスタンス化する際に必要なオブジェクトを注入します。
これによりそのクラスが依存してるクラスを自身でインスタンス化する必要がなくなり疎結合なクラスを作る事ができます。
また外から注入する事でテストがしやすくなる利点があります。

DIのハマりポイント

ここからは実際に僕がハマったポイントを紹介していきたいと思います。
ドキュメントを読んでていてもPHP脳で理解が浅いとハマります…。

スコープが違う

サーバプロセスに起因する問題ではありますが.NETのDIのスコープ(インスタンスのライフサイクル)はSingleton, Scoped, Transientの3種類が用意されています。
Singletonはサーバの起動(プロセス)単位、Scopedはサーバへのリクエスト単位、Transientはインスタンス単位になっています。

https://learn.microsoft.com/ja-jp/dotnet/core/extensions/dependency-injection#service-lifetimes

PHPはApacheにしてもphp-fpmにしてもリクエスト単位の処理になるため、Singletonでもリクエスト単位のライフサイクルになります。
これを理解しないままPHP脳で.NETでSingletonを使うと意図しない動きになります。

例えばリクエスト単位でインメモリにデータを蓄積したいような場面で
.NETでSingletonを使うとずっとクリアされない状態となりバグを生み出します。

基本はScopedで必要に応じてSingletonを使う事を意識すると良いです。
例えばコネクションプールを保持するクラス等はSingletonを使う等です。

スコープのバリデートがある

PHPでいくとスコープのバリデートって何ぞやってなりますよね。

先程説明したように.NETのDIのスコープが3つあります。
そしてスコープによっては注入してはいけないスコープが存在しています。

例えばSingletonにはScopedのインスタンスを入れてはいけません。
これはSingletonがサーバの起動単位であるのに対し、Scopedがリクエスト単位の起動であるため
Scopedのインスタンスをリクエスト単位で注入できないためです。

このルールをチェックするための機構がバリデートになります。

https://learn.microsoft.com/ja-jp/aspnet/core/fundamentals/host/web-host?view=aspnetcore-7.0#scope-validation

ただバリデートが動かない環境であれば通常通り動いてしまうので潜在的なバグを仕込むことになりかねません。
バリデート自体はデフォルトでONになっており.NETの環境変数のEnvironmentNameDevelopmentの時に動くようになっています。
これを知らないと以下のエラーになって起動途中でエラー落ちします。

System.InvalidOperationException: Cannot consume scoped service 'Hoge' from singleton 'Fuga'

例えばローカルの環境名をLocalなどにしているとバリデートが動かないので
Developmentが設定されている開発環境にデプロイしてから初めて気づくという事態になります。

バリデートを動かす環境は制御出来るので以下のようにして早期に発見できるようにしましょう。

namespace Hoge
{
    public static class Program
    {
        private static IWebHostBuilder CreateWebHostBuilder(string[] args)
        {
            return WebHost.CreateDefaultBuilder(args)
                ......
                .UseDefaultServiceProvider((context, options) =>
                {
                    // 有効にしたい環境の時だけtrueを設定する
                    options.ValidateScopes = context.HostingEnvironment.EnvironmentName == "Local";
                })
                .UseStartup<Startup>();
        }
    }
}

DI設定時に重くなる可能性がある同期処理をしてはいけない

DI時に予めDBなどにセットされた値を取得してインスタンスにセットした状態で
DIコンテナに登録したい場合ってあったりしますよね。

ただこれをC#で安易にやってしまうとサーバがハングアップする事があるのでやってはいけません。

C#の同期処理はスレッドで行われるので、遅い処理があるとスレッドをブロックしてしまい
アクセスが多いとあっという間にスレッドを使い果たしてハングアップします。

dotnet標準のMicrosoft.Extensions.DependencyInjectionでは非同期処理をサポートしてないので
避けるには以下の形でProgram.csのMain関数を非同期にしてRunする前に実行するようにします。

namespace Hoge
{
    public static class Program
    {
        public static async Task Main(string[] args)
        {
            var host = CreateWebHostBuilder(args).Build();
            using (var scope = host.Services.CreateScope())
            {
                var hogeService = scope.ServiceProvider.GetRequiredService<HogeService>();
                await hogeService.HandleAsync(CancellationToken.None).ConfigureAwait(false);
            }

            await host.RunAsync().ConfigureAwait(false);
        }

        .......
    }
}

まとめ

ここまでPHP開発者がC#(.NET)で実装した時のDIのハマりポイントについて書いてきました。

PHPだとあまりDIのスコープやスレッドを意識する事はなかったのでとても勉強になりました。
今回のハマりポイント以外は特にハマる所はなかったのでここさえ抑えておけばC#(.NET)でも特に困らず書けるかなと思います。

もちろん言語構造が違うので覚える事はたくさんありますが
PHPで培ったノウハウはC#でも活かせるので機会がありましたら是非C#で書いてみてはいかがでしょうか。

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!