アプリケーション内で依存性の注入を管理する方法は多数ありますが、さまざまなアプローチの利点と制限を理解することが重要だと思います。推奨されているAutofacServiceProviderFactory
を使用する代わりに、ASP.NET Core で Autofac ContainerBuilder
依存性を構造化する主な方法として使用することは、検討できる方法の 1 つです。
この記事では、AutofacServiceProviderFactory の代わりに、ASP.NET Core アプリケーションで Autofac の ContainerBuilder を使用する方法について説明します。依存性注入で利用できる他のアプローチと比較して、このアプローチでできることとできないことについて説明します。
これは、ASP.NET Core 内で Autofac を使用して依存関係の解決を検討するシリーズの一部です。問題が公開されたら、以下のシリーズも必ず含めます。
このシリーズの最後には、ASP.NET Core と Blazor 内のプラグイン アーキテクチャをより自信を持って探索できるようになり、探索できるコンテンツがさらに増えます。
公平に言えば、このセクションのタイトルは*ほぼ*クリックベイトです。ASP.NET Core アプリケーションで Autofac を設定するための推奨方法としてAutofacServiceProviderFactory
を使用することは、ほとんどのアプリケーションにとって最適だと思います。依存性注入フレームワークとして Autofac を使用したい開発者の大多数は、この方法ではほとんど問題に遭遇しません。
それは私たちに次の能力を与えてくれます:
WebApplicationBuilder
(および現時点で利用可能なもの)にアクセスしますIConfiguration
のインスタンスへのアクセス ( WebApplicationBuilder
インスタンスからも利用可能)
しかし、私にとって大きな問題は、 WebApplication
インスタンスにアクセスできないことです。C# でプラグイン アーキテクチャを構築する場合、特に ASP.NET Core アプリケーションを構築する場合は、ルートを登録するためにWebApplication
インスタンスにアクセスできるようにしたいです。これにより、プラグインから最小限の API を簡単に登録できます。技術的には、便利な構文を取得するためにIEndpointRouteBuilder
の実装にアクセスするだけで済みます。
これなしで非最小限の API を登録できますか? もちろんです。同様の構文を提供し、 WebApplication
インスタンスを必要としない別の方法はありますか? おそらくあります。しかし、その問題を回避しようとする代わりに、興味のある依存関係にアクセスできるかどうかを確認したいと思いました。
依存関係コンテナの設定方法に関する計画を変更する時期が来ました。
共通の基盤を探るために、サンプル アプリケーションを見てみましょう。 前回の記事を読んだことがあれば、これはサンプル天気予報アプリケーションのバリエーションに似ているでしょう。
using Autofac; using Microsoft.AspNetCore.Mvc; // personal opinion: // I absolutely love having the entry point of my // applications being essentially: // - make my dependencies // - give me the primary dependency // - use it // - ... nothing else :) ContainerBuilder containerBuilder = new(); containerBuilder.RegisterModule<MyModule>(); using var container = containerBuilder.Build(); using var scope = container.BeginLifetimeScope(); var app = scope.Resolve<WebApplication>(); app.Run(); internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } internal sealed class MyModule : Module { protected override void Load(ContainerBuilder containerBuilder) { containerBuilder.RegisterType<DependencyA>().SingleInstance(); containerBuilder.RegisterType<DependencyB>().SingleInstance(); containerBuilder.RegisterType<DependencyC>().SingleInstance(); containerBuilder .Register(ctx => { var builder = WebApplication.CreateBuilder(Environment.GetCommandLineArgs()); return builder; }) .SingleInstance(); containerBuilder .Register(ctx => ctx.Resolve<WebApplicationBuilder>().Configuration) .As<IConfiguration>() .SingleInstance(); containerBuilder .Register(ctx => { var builder = ctx.Resolve<WebApplicationBuilder>(); var app = builder.Build(); app.UseHttpsRedirection(); // FIXME: the problem is that the Autofac ContainerBuilder // was used to put all of these pieces together, // but we never told the web stack to use Autofac as the // service provider. // this means that the minimal API will never be able to // find services off the container. we would need to resolve // them BEFORE the API is called, like in this registration // method itself, from the context that is passed in. //DependencyA dependencyA = ctx.Resolve<DependencyA>(); // FIXME: But... What happens if something wants to take a // dependency on the WebApplication instance itself? Once the // web application has been built, there's no more adding // dependencies to it! var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet( "/weatherforecast", ( [FromServices] DependencyA dependencyA , [FromServices] DependencyB dependencyB , [FromServices] DependencyC dependencyC ) => { var forecast = Enumerable .Range(1, 5) .Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }); return app; }) .SingleInstance(); } } internal sealed class DependencyA( WebApplicationBuilder _webApplicationBuilder); internal sealed class DependencyB( WebApplication _webApplication); internal sealed class DependencyC( IConfiguration _configuration);
このアプローチの 1 つの特徴は、Autofac ContainerBuilder
クラスを主要な依存関係コンテナーとして使用すると、エントリ ポイントを次のように構成できる点です。
これは、私の意見では、理想的なアプリケーション起動コードです。なぜでしょう? それは、ここに戻って変更する必要がないからです。決して。追加するものがいくつあってもです! アセンブリをスキャンして、より多くのモジュールをロードできるからです。
繰り返しますが、これは私の個人的な好みであり、これがすべての人の目標であるべきだと主張しようとしているわけではありません。
もちろん、これは完全に完璧というわけではない別のアプローチです。それでは、Autofac のこの設定で得られない点について説明しましょう。
WebApplication
、依存性注入フレームワークとして Autofac を使用するように構成されていません。WebApplication
インスタンスをまだ取得できません... そのため、これへのアクセスに関しては特に進歩はありませんでした。
でも、大体はこれだけです。欠点のリストはひどいものではありませんが、Autofac ContainerBuilder アプローチは私たちにとって万能の解決策ではありませんでした。それで、私たちは何を得たのでしょうか? 次の点についても説明するのに役立ちます。
私たちが行うことすべてには長所と短所があります。ASP.NET Core の Autofac ContainerBuilder の問題を確認したので、ここからどのような進歩が得られたかを見てみましょう。
WebApplicationBuilder
およびIConfiguration
インスタンスに引き続きアクセスできるため、これはAutofacServiceProviderFactory
アプローチを使用する場合と同等の利点です。Autofac 登録メソッド内で最小限の API を登録できることはわかりましたが、残念ながら、最小限の API 呼び出し自体でコンテナーからの依存関係を直接解決することはできません。依存関係が自動的に解決されるルート定義を処理する、次のような専用クラスを構築することができます。
internal sealed class WeatherForecastRoutes( DependencyA _dependencyA // FIXME: still can't depend on this because // we can't get the WebApplication //, DependencyB _dependencyB , DependencyC _dependencyC) { private static readonly string[] _summaries = new[] { "Freezing", "Bracing", // ... }; public WeatherForecast[] Forecast() { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), _summaries[Random.Shared.Next(_summaries.Length)] )) .ToArray(); return forecast; } }
このクラスと依存関係がすべて同じコンテナ上にある場合、自動解決が行われます。その後は、次のコードを呼び出すだけです。
var weatherForecastRoutes = ctx.Resolve<WeatherForecastRoutes>(); app.MapGet("/weatherforecast2", weatherForecastRoutes.Forecast);
これは、プラグインの観点から見ると、まだ少し面倒です。なぜなら、このように登録コードを呼び出すためだけに、すべてのルート クラスを手動で解決する必要があるからです。これはすべて、これらのものがWebApplication
インスタンスへの独自のアクセスを解決できないという事実に起因しています。
しかし、待ってください... 状況を逆にするとどうなるでしょうか? IRegisterRoutes
のようなインターフェースを解決して、 WebApplication
インスタンスを渡すことができたらどうなるでしょうか?
// NOTE: make sure to register WeatherForecastRouteRegistrar on the // autofac container as IRegisterRoutes! internal interface IRegisterRoutes { void RegisterRoutes(WebApplication app); } internal sealed class WeatherForecastRouteRegistrar( WeatherForecastRoutes _weatherForecastRoutes) : IRegisterRoutes { public void RegisterRoutes(WebApplication app) { app.MapGet("/weatherforecast2", _weatherForecastRoutes.Forecast); } } // TODO: add this to the autofac code where the // WebApplication instance is built: foreach (var registrar in ctx.Resolve<IEnumerable<IRegisterRoutes>>()) { registrar.RegisterRoutes(app); }
これで、 WebApplication
インスタンスがプラグインにアクセスできるかどうかを気にする必要さえなくなりました。最初のバージョンは結局それほど制限がなかったのかもしれません。何かいいことを思いついたのかもしれません... ただし、次の記事でこれについてさらに詳しく説明します。
この記事では、通常提案されているAutofacServiceProviderFactory
を使用する代わりに、Autofac ContainerBuilder
明示的に使用する方法について説明しました。同様の利点と欠点を確認しましたが、考慮すべき点もいくつかありました。それぞれの方法には、アプリケーションで何を求めているかに応じて長所と短所があります。
興味深いのは、プラグインに向けて作業しようとしている場合、プラグインからWebApplication
インスタンスにアクセスする必要さえなくなるかもしれないということです。最小限の API を重視する場合、これはまだ制限があるかもしれませんが... そうでなければ、興味深い考え方に至っています。
これが役に立ち、さらに学習の機会を探している場合は、毎週無料で配信される私のソフトウェア エンジニアリング ニュースレターの購読を検討し、 YouTube で私の無料ビデオをチェックしてください。