.NET Coreを用いた軽量型IoCコンテナの実装と設計

汎用APIフレームワークの拡張モジュールとして、依存性逆転の原則に基づくオブジェクト解決機能が必要となった。既存のサードパーティ製ライブラリは機能豊富だが、本アーキテクチャに対して過剰であるため、要件に特化した最小限のIoCコンテナを独自実装する。以下では、アセンブリの動的解決、リフレクションを活用したオブジェクト生成、および設定ファイル駆動の初期化ロジックについて解説する。

設計要件とアーキテクチャ

本モジュールの実装にあたり、以下の機能要件を設定する。

  • 任意のディレクトリをアセンブリ探索パスとして追加可能(デフォルトは実行ファイル配置ディレクトリ)
  • クラスの完全修飾名およびコンストラクタ引数の可変長配列に基づいたインスタンス生成
  • 設定ファイル内の識別子に対応したオブジェクトの構築とプロパティ注入
  • 単一インスタンスのキャッシング制御
  • 初期化メソッドの自動呼び出し(実装保留)

探索パスの管理実装

アセンブリ探索対象のディレクトリパスはスレッドセーフなコレクションで管理する。外部からの追加・削除リクエストは排他制御により整合性を保つ。

using System.Collections.Generic;

namespace Framework.DependencyInjection
{
    public static class DependencyResolver
    {
        private static readonly List<string> _resolutionPaths = new();
        private static readonly object _pathLock = new();

        public static void RegisterAssemblyDirectory(string directoryPath)
        {
            if (string.IsNullOrWhiteSpace(directoryPath)) return;
            lock (_pathLock)
            {
                var normalizedPath = directoryPath.TrimEnd('\\', '/').ToLowerInvariant();
                if (!_resolutionPaths.Contains(normalizedPath))
                    _resolutionPaths.Add(normalizedPath);
            }
        }

        public static void UnregisterAssemblyDirectory(string directoryPath)
        {
            lock (_pathLock)
                _resolutionPaths.Remove(directoryPath.ToLowerInvariant());
        }
    }
}

動的オブジェクト生成とシングルトン制御

インスタンス生成処理は、呼び出し元が指定した完全修飾名を基にアセンブリを検索し、リフレクション経由でコンストラクタを実行する。シングルトン制御には二重チェックロッキングを採用し、並列実行環境下でのパフォーマンス劣化を防ぐ。

using System;
using System.Collections.Concurrent;
using System.Reflection;

namespace Framework.DependencyInjection
{
    public static class DependencyResolver
    {
        private static readonly ConcurrentDictionary<string, object> _singletonRegistry = new();
        private static readonly object _singletonGuard = new();

        public static object ResolveInstance(string fullyQualifiedName, params object[] constructorArgs)
        {
            return AssemblyScanner.BuildTarget(fullyQualifiedName, constructorArgs);
        }

        public static object ResolveSingleton(string fullyQualifiedName, params object[] constructorArgs)
        {
            var cacheKey = fullyQualifiedName.ToLowerInvariant();
            if (_singletonRegistry.TryGetValue(cacheKey, out var cachedInstance))
                return cachedInstance;

            lock (_singletonGuard)
            {
                if (!_singletonRegistry.TryGetValue(cacheKey, out cachedInstance))
                {
                    var newInstance = AssemblyScanner.BuildTarget(fullyQualifiedName, constructorArgs);
                    if (newInstance != null)
                        _singletonRegistry.AddOrUpdate(cacheKey, newInstance, (k, v) => newInstance);
                    cachedInstance = newInstance;
                }
            }
            return cachedInstance;
        }
    }
}

実際の探索・生成ロジックは AssemblyScanner クラスに集約する。まず実行中のドメインに読み込み済みアセンブリを対象に型名を走査し、存在しない場合は登録済みの検索パスからDLLファイルを列挙して動的に読み込む。一致する型が見つかった場合、引数有無に応じて適切なコンストラクタをバインドする。

internal static class AssemblyScanner
{
    private static readonly ConcurrentDictionary<string, Assembly> _typeCache = new();
    private static readonly ConcurrentDictionary<string, bool> _loadedFiles = new();
    private static readonly object _pathReadLock = new();

    internal static object BuildTarget(string targetTypeName, object[]? arguments = null)
    {
        var assembly = ResolveAssembly(targetTypeName);
        if (assembly == null) return null;

        return arguments == null || arguments.Length == 0
            ? assembly.CreateInstance(targetTypeName, true)
            : assembly.CreateInstance(targetTypeName, true, BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public, null, arguments, null, null);
    }

    private static Assembly ResolveAssembly(string targetTypeName)
    {
        if (_typeCache.TryGetValue(targetTypeName.ToLowerInvariant(), out var cached))
            return cached;

        foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
        {
            if (ContainsType(asm, targetTypeName))
            {
                _typeCache.TryAdd(targetTypeName.ToLowerInvariant(), asm);
                return asm;
            }
        }
        return ScanDiskForAssembly(targetTypeName);
    }

    private static Assembly ScanDiskForAssembly(string targetTypeName)
    {
        var searchLocations = new List<string> { AppDomain.CurrentDomain.BaseDirectory };
        lock (_pathReadLock)
            searchLocations.AddRange(DependencyResolver.GetPathsSnapshot());

        foreach (var directory in searchLocations)
        {
            foreach (var filePath in Directory.EnumerateFiles(directory, "*.dll", SearchOption.AllDirectories))
            {
                var normalizedPath = filePath.ToLowerInvariant();
                if (_loadedFiles.ContainsKey(normalizedPath)) continue;

                try
                {
                    var asm = Assembly.LoadFrom(filePath);
                    _loadedFiles.TryAdd(normalizedPath, true);
                    if (ContainsType(asm, targetTypeName))
                    {
                        _typeCache.TryAdd(targetTypeName.ToLowerInvariant(), asm);
                        return asm;
                    }
                }
                catch { }
            }
        }
        return null;
    }

    private static bool ContainsType(Assembly assembly, string typeName) => assembly.GetType(typeName) != null;
}

XML設定ファイルによる構成駆動型生成

複雑なオブジェクトグラフの初期化は、ハードコーディングを避けXML設定ファイルで管理する。コンストラクタ引数の型推論、基本型およびコレクション型のデシリアライズ、プロパティへの値注入を実装する。

<serviceDefinitions>
  <service identifier="personSvc" implementationType="CoreLib.Models.Person">
    <constructorParam type="string">JohnDoe</constructorParam>
    <injectProperty name="DisplayName">山田太郎</injectProperty>
  </service>
</serviceDefinitions>

ファイル解析後、識別子をキーとして設定オブジェクトをキャッシュする。解決要求時には、XMLから抽出した引数配列をビルドし、既存の生成メソッドへ委譲する。プロパティ注入段階では、リフレクションを用いてパブリックなアクセサーを検出し、設定値を型変換して割り当てる。

public static object ResolveFromConfiguration(string configId)
{
    var configEntry = ConfigurationStore.Retrieve(configId);
    ValidateConfiguration(configEntry, configId);

    var ctorParams = ConfigurationMapper.ExtractArguments(configEntry);
    var instance = ResolveInstance(configEntry.ImplementationType, ctorParams);
    ConfigurationMapper.ApplyProperties(configEntry, instance);

    return instance;
}

本アーキテクチャにより、ResolveInstance および ResolveFromConfiguration を通じて、単純な型名指定から設定ファイル駆動の複雑な初期化まで統一的なインターフェースでオブジェクトを取得できる。型指定ジェネリック版のオーバーロードやシングルトン解決メソッドも同様のパターンで実装可能である。次段階の課題として、設定ファイル内のメソッド呼び出し定義のサポートおよびベンチマークによるパフォーマンス評価が挙げられる。

タグ: dependency-injection csharp-reflection assembly-resolution xml-parsing dotnet-core

8月15日 08:09 投稿