今からでも間に合う

技術を学ぶのは今からでも遅くない

IServiceCollectionとコンストラクターインジェクションの作りを理解する

BlazorというかASP触り始めてまだ数日ですが、IServiceCollectionコンストラクターインジェクションの動きがわからなかったので自分で実装してみました。
仕組みに興味がある方の参考になればと思います。

環境

  • VS2022
  • .NET6
  • コンソールアプリ

前提知識

DI(Dependency Injection)自体の説明はしません。
仕組みや設計の理解がメインなので。

早速書いてみました

本来の仕組みとしては、Microsoft.Extensions.DependencyInjectionにあります。
厳密に知りたい人はそちらを解析してください。

App
public class App
{
    private readonly ServiceProvider _provider;
    public static IAppBuilder CreateBuilder() => new AppBuilder();

    internal App(ServiceProvider provider) => _provider = provider;

    public TService GetService<TService>() where TService : class
    {
        return _provider.GetService<TService>();
    }
}

アプリケーションのメインとなるクラス。
自分自身を構築するためのBuilderを生成し、最終的にBuilderを介してインスタンスを作ってもらいます。

ServiceProvider
internal class ServiceProvider
{
    private readonly IServiceCollection _services;
    public ServiceProvider(IServiceCollection services)
    {
        _services = services;
    }
    public TService GetService<TService>() where TService : class
    {
        return _services.GetRequiredService<TService>();
    }
}

サンプルでは素通しするだけになっていますが、ここでServiceを提供するための振り分けやらを行います。

IAppBuilder
public interface IAppBuilder
{
    IServiceCollection Services { get; }

    App Build();
}

アプリケーションを構築するためのBuilderパターンインターフェース。
ここではServiceの構築のみ作ってみていますが、やり方は同等でいけるでしょう。

AppBuilder
internal class AppBuilder : IAppBuilder
{
    public IServiceCollection Services { get; }

    public AppBuilder()
    {
        Services = new ServiceCollection();
    }
    public App Build()
    {
        //ここでServiceCollectionの依存関係とかごにょごにょ解決すると思う
        return new App(new ServiceProvider(Services));
    }
}

IAppBuilderの具象クラス。外部に見せたくないのでinternalです。
Build()の呼び出しにより、それまでに登録された情報の依存関係を解決させたり事前にインスタンス作ってインフラ構築したりと準備処理をする。

IServiceCollection
public interface IServiceCollection
{
    void AddSingleton<TService, TImpl>();
    void AddSingleton<TService>();
    TService GetRequiredService<TService>();
}

今回の主題ですね。
Add系が依存関係の登録、Get系が依存関係を解決した結果の取得になっています。
インスタンスの生存期間によって異なる登録の仕方はありますが、ここではシングルトンのみ対応してみます。
とはいっても、↓の実装ではシングルトンになっていませんが。。。

ServiceCollection
internal class ServiceCollection : IServiceCollection
{
    private readonly HashSet<Type> _services;
    private readonly Dictionary<Type, Type> _singletons;

    public ServiceCollection()
    {
        _services = new HashSet<Type>();
        _singletons = new Dictionary<Type, Type>();
    }

    public void AddSingleton<TService>()
    {
        var service = typeof(TService);
        _services.Add(service);
    }

    public void AddSingleton<TService, TImpl>()
    {
        var service = typeof(TService);
        var impl = typeof(TImpl);
        if (_singletons.ContainsKey(service))
        {
            _singletons[service] = impl;//いったん後勝ちにしておく
        }
        _singletons.Add(service, impl);
    }

    public TService GetRequiredService<TService>()
    {
        if (!_services.TryGetValue(typeof(TService), out var target))
        {   
            throw new InvalidOperationException();
        }

        //実験したかったパターンが通るだけの実装
        foreach (var constructor in target.GetConstructors())
        {
            foreach (var m in constructor.GetParameters())
            {
                if (_singletons.ContainsKey(m.ParameterType))
                {
                    var impl = _singletons[m.ParameterType];
                    var arg = new[] { impl.Assembly.CreateInstance(impl.FullName) };
                    return (TService)(target.Assembly.CreateInstance(
                        target.FullName,
                        false,
                        System.Reflection.BindingFlags.Default,
                        null,
                        arg,
                        null,
                        null));
                }
            }
        }
        return default(TService);
    }
}

予想はしてましたが(ほかに選択肢が思いつきませんが)リフレクションで実現できました。
あまりリフレクション使っての開発って機会がないのですが意外とサクッと動きました。

呼び出し側

var builder = App.CreateBuilder();

builder.Services.AddSingleton<ISample, Sample>();
builder.Services.AddSingleton<Service>();

var app = builder.Build();

var service = app.GetService<Service>();
service.DoSomething();

interface ISample
{
    string Name { get; set; }
}

class Sample : ISample
{
    public string Name { get; set ; }
}

class Service
{
    private readonly ISample _sample;

    public Service(ISample sample)
    {
        _sample = sample;
    }

    public void DoSomething() => Debug.WriteLine(_sample.GetType());
}

これで 実行すると、無事「Sample」が出力されました。

おしまい

プラグイン的な作りでだいぶ疎結合だとは思いますが、最初のインフラ構築以外では積極的に使いたいとは思いませんでした。
こんな設計が実稼働中のアプリでバリバリに動いてたら不具合があったときに追いにくくて仕方ないだろうなと。

プライバシーポリシー


d払いポイントGETモール