ASP.NET Coreの条件付き依存関係注入。 パート2









この記事の最初の部分では 、環境および構成メカニズムを使用して条件付き依存性注入を実装し、リクエストデータに基づいてHTTPリクエストの一部としてサービスを受信するための依存性インジェクターの設定を示しました。



2番目の部分では、アプリケーションの実行中に識別子によって必要な実装を選択する例を使用して、依存関係インジェクターの機能を拡張する方法を説明します。



識別子によるサービスの取得(IDによるサービスの解決)



多くの一般的なIoCフレームワークは、おおよそ次の機能を提供し、インターフェイスを実装する特定のタイプに名前を付けることができます。



var container = new UnityContainer(); //     Unity... container.RegisterType<IService, LocalService>("local"); container.RegisterType<IService, CloudService>("cloud"); IService service; if (context.IsLocal) { service = container.Resolve<IService>("local"); } else { service = container.Resolve<IService>("cloud"); }
      
      





または:



 public class LocalController { public LocalController([Dependency("local")] IService service) { this.service = service; } } public class CloudController { public CloudController([Dependency("cloud")] IService service) { this.service = service; } }
      
      





これにより、コンテキストに応じて、必要な実装を選択できます。



ASP.NET Coreに組み込まれている依存関係インジェクターは複数の実装をサポートしていますが、残念ながら、単一の実装に識別子を割り当てる機能はありません。 幸いなことに:)コードを書くことで、自分で識別子を使ってサービスの解決を実装できます。



この機能を実装する1つの方法は、 ServiceName



プロパティでServiceDescriptor



クラスを拡張し、それを使用してサービスを取得することでした。 しかし、ソースコードを調べた後、 ServiceProvider



実装へのアクセスが閉じられていることが明らかになり(クラスには内部アクセス修飾子があります)、 GetService



メソッドのロジックを変更することはできません。



リフレクションを使用するという考えを放棄し、独自のServiceProvider



を記述することで、サービスを受信するときに使用するために、サービスの名前とタイプの対応の構造をコンテナに直接保存することにしました。 サービスの受信ロジックに影響を与える方法については、記事の最初の部分で説明します。



最初に、識別子のマッピングと各インターフェイスの対応する実装を含む構造を作成します。



これはこの種の辞書になります。



 Dictionary<Type, Dictionary<string, Type>>
      
      





ここで、ディクショナリキーはインターフェイスのタイプになり、値はディクショナリになります(トートロジーについて謝罪します)。キーは識別子になり、値はインターフェイス実装のタイプになります。



次のように、この構造にサービスを追加します。



 private readonly Dictionary<Type, Dictionary<string, Type>> serviceNameMap = new Dictionary<Type, Dictionary<string, Type>>(); public void RegisterType(Type service, Type implementation, string name) { if (this.serviceNameMap.ContainsKey(service)) { var serviceNames = ServiceNameMap[service]; if (serviceNames.ContainsKey(name)) { /* overwrite existing name implementation */ serviceNames[name] = implementation; } else { serviceNames.Add(name, implementation); } } else { this.serviceNameMap.Add(service, new Dictionary<string, Type> { [name] = implementation }); } }
      
      





これが、コンテナからサービスを受け取る方法です( 前の記事で覚えているように、ASP.NET CoreのIoCコンテナはIServiceProvider



インターフェイスによって表示されます)。



 public object Resolve(IServiceProvider serviceProvider, Type serviceType, string name) { var service = serviceType; if (service.GetTypeInfo().IsGenericType) { return this.ResolveGeneric(serviceProvider, serviceType, name); } var serviceExists = this.serviceNameMap.ContainsKey(service); var nameExists = serviceExists && this.serviceNameMap[service].ContainsKey(name); /* Return `null` if there is no mapping for either service type or requested name */ if (!(serviceExists && nameExists)) { return null; } return serviceProvider.GetService(this.serviceNameMap[service][name]); }
      
      





コンテナを簡単に設定するための拡張メソッドのセットを記述することは、次のとおりです。



 public static IServiceCollection AddScoped<TService, TImplementation>(this IServiceCollection services, string name) where TService : class where TImplementation : class, TService { return services.Add(typeof(TService), typeof(TImplementation), ServiceLifetime.Scoped, name); }
      
      





 private static IServiceCollection Add(this IServiceCollection services, Type serviceType, Type implementationType, ServiceLifetime lifetime, string name) { var namedServiceProvider = services.GetOrCreateNamedServiceProvider(); namedServiceProvider.RegisterType(serviceType, implementationType, name); services.TryAddSingleton(namedServiceProvider); services.Add(new ServiceDescriptor(implementationType, implementationType, lifetime)); return services; } private static NamedServiceProvider GetOrCreateNamedServiceProvider(this IServiceCollection services) { return services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(NamedServiceProvider))?.ImplementationInstance as NamedServiceProvider ?? new NamedServiceProvider(); }
      
      





上記のコードでは、型と名前の間の対応の構造に識別子を追加し、単純に実装型をコンテナに配置します。 識別子によるサービスの取得方法:



 public static TService GetService<TService>(this IServiceProvider serviceProvider, string name) where TService : class { return serviceProvider .GetService<NamedServiceProvider>() .Resolve<TService>(serviceProvider, name); }
      
      





すべてが使用可能です。



 services.AddScoped<IService, LocalhostService>("local"); services.AddScoped<IService, CloudService>("cloud"); var service1 = this.serviceProvider.GetService<IService>("local"); // resolves LocalhostService var service2 = this.serviceProvider.GetService<IService>("cloud"); // resolves CloudService
      
      





少し進んで、次の構文のMVCコア[FromServices]



属性のように、アクションパラメーターに挿入できる属性を作成できます。



 public IActionResult Local([FromNamedServices("local")] IService service) { ... }
      
      





このアプローチを実装するには、ASP.NET Coreでのモデルバインディングのプロセスをもう少し深く理解する必要があります



つまり、パラメーター属性は、どのModelBinder( IModelBinder



インターフェイスを実装するクラス)がパラメーターオブジェクトを作成するかを決定します。 たとえば、ASP.NET Core MVCの一部である[FromServices]



属性は、モデルをバインドするためにIoCコンテナーが使用されることを示します。したがって、このパラメーターにはServicesModelBinder



クラスが使用され、IoCコンテナーからパラメータータイプを取得しようとします。



この場合、2つの追加クラスを作成します。 1つ目はModelBinder



、識別子によってIoCコンテナーからサービスを受け取りますFromNamedServices



つ目は、コンストラクターでサービス識別子を受け入れる独自のFromNamedServices



属性で、作成した特定のModelBinder



をバインドに使用することを示します。



 [AttributeUsage(AttributeTargets.Parameter)] public class FromNamedServicesAttribute : ModelBinderAttribute { public FromNamedServicesAttribute(string serviceName) { this.ServiceName = serviceName; this.BinderType = typeof(NamedServicesModelBinder); } public string ServiceName { get; set; } public override BindingSource BindingSource => BindingSource.Services; }
      
      





 public class NamedServicesModelBinder : IModelBinder { private readonly IServiceProvider serviceProvider; public NamedServicesModelBinder(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); var serviceName = GetServiceName(bindingContext); if (serviceName == null) return Task.FromResult(ModelBindingResult.Failed()); var model = this.serviceProvider.GetService(bindingContext.ModelType, serviceName); bindingContext.Model = model; bindingContext.ValidationState[model] = new ValidationStateEntry { SuppressValidation = true }; bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } private static string GetServiceName(ModelBindingContext bindingContext) { var parameter = (ControllerParameterDescriptor)bindingContext .ActionContext .ActionDescriptor .Parameters .FirstOrDefault(p => p.Name == bindingContext.FieldName); var fromServicesAttribute = parameter ?.ParameterInfo .GetCustomAttributes(typeof(FromServicesAttribute), false) .FirstOrDefault() as FromServicesAttribute; return fromServicesAttribute?.ServiceName; } }
      
      





これですべてです:)例のソースコードは、リンクからダウンロードできます。



github.com/nix-user/AspNetCoreDI



All Articles