非同期の初期化が必要なタイプConnections
があります。このタイプのインスタンスは、他のいくつかのタイプ(たとえば、Storage
)によって消費されます。各タイプには、非同期初期化も必要です(インスタンスごとではなく静的であり、これらの初期化もConnections
に依存します)。 。最後に、私のロジックタイプ(例:Logic
)はこれらのストレージインスタンスを消費します。現在、SimpleInjectorを使用しています。
私はいくつかの異なる解決策を試しましたが、常にアンチパターンが存在します。
私が現在使用しているソリューションには、TemporalCouplingアンチパターンがあります。
public sealed class Connections
{
Task InitializeAsync();
}
public sealed class Storage : IStorage
{
public Storage(Connections connections);
public static Task InitializeAsync(Connections connections);
}
public sealed class Logic
{
public Logic(IStorage storage);
}
public static class GlobalConfig
{
public static async Task EnsureInitialized()
{
var connections = Container.GetInstance<Connections>();
await connections.InitializeAsync();
await Storage.InitializeAsync(connections);
}
}
Temporal Couplingをメソッドにカプセル化したので、それほど悪くはありません。しかし、それでも、それはアンチパターンであり、私が望むほど保守可能ではありません。
一般的に提案されているソリューションは、AbstractFactoryパターンです。ただし、この場合は非同期初期化を扱っています。したがって、私couldは、初期化を強制的に同期的に実行することでAbstract Factoryを使用できますが、これはsync-over-asyncアンチパターンを採用します。私は複数のストレージを持っていて、現在のコードではそれらがすべて同時に初期化されるため、sync-over-asyncアプローチが本当に嫌いです。これはクラウドアプリケーションであるため、これをシリアル同期に変更すると起動時間が長くなり、リソースを消費するため、パラレル同期も理想的ではありません。
非同期ファクトリメソッドでAbstractFactoryを使用することもできます。ただし、このアプローチには1つの大きな問題があります。 Mark Seemanがコメントしているように ここ 、「正しく登録すれば、その塩に値するDIコンテナはすべて[ファクトリ]インスタンスを自動配線できます。」残念ながら、これは非同期ファクトリには完全に当てはまりません。これをサポートするnoDIコンテナがあります。
したがって、Abstract Asynchronous Factoryソリューションでは、少なくともFunc<Task<T>>
で明示的なファクトリを使用する必要があり、 これはどこにでもあることになります ( "私たちは個人的に、Funcデリゲートを登録できるようにすることでデフォルトはデザインの匂いです... Funcに依存するコンストラクターがシステムに多数ある場合は、依存関係の戦略をよく見てください。 "):
public sealed class Connections
{
private Connections();
public static Task<Connections> CreateAsync();
}
public sealed class Storage : IStorage
{
// Use static Lazy internally for my own static initialization
public static Task<Storage> CreateAsync(Func<Task<Connections>> connections);
}
public sealed class Logic
{
public Logic(Func<Task<IStorage>> storage);
}
これはそれ自身のいくつかの問題を引き起こします:
CreateAsync
に渡す必要があります。つまり、DIコンテナは、依存性注入を実行しなくなりました。もう1つの、あまり一般的ではない解決策は、型の各メンバーに独自の初期化を待機させることです。
public sealed class Connections
{
private Task InitializeAsync(); // Use Lazy internally
// Used to be a property BobConnection
public X GetBobConnectionAsync()
{
await InitializeAsync();
return BobConnection;
}
}
public sealed class Storage : IStorage
{
public Storage(Connections connections);
private static Task InitializeAsync(Connections connections); // Use Lazy internally
public async Task<Y> IStorage.GetAsync()
{
await InitializeAsync(_connections);
var connection = await _connections.GetBobConnectionAsync();
return await connection.GetYAsync();
}
}
public sealed class Logic
{
public Logic(IStorage storage);
public async Task<Y> GetAsync()
{
return await _storage.GetAsync();
}
}
ここでの問題は、Temporal Couplingに戻ったことです。今回は、システム全体に分散しています。また、このアプローチでは、allパブリックメンバーが非同期メソッドである必要があります。
したがって、ここで対立する2つのDI設計の観点が実際にあります。
問題は、特に非同期初期化の場合、DIコンテナーが「単純なコンストラクター」アプローチに固執する場合、ユーザーに独自の初期化を他の場所で実行させ、独自のアンチパターンをもたらすことです。例: Simple Injectorが非同期関数を考慮しない理由 : "いいえ、このような機能は、依存性に関していくつかの重要な基本ルールに違反しているため、SimpleInjectorやその他のDIコンテナーには意味がありません。注入。"ただし、厳密に「基本ルールに従って」プレイすると、明らかに、はるかに悪いと思われる他のアンチパターンが強制されます。
質問:すべてのアンチパターンを回避する非同期初期化のソリューションはありますか?
更新:AzureConnections
の完全な署名(上記ではConnections
と呼ばれます):
public sealed class AzureConnections
{
public AzureConnections();
public CloudStorageAccount CloudStorageAccount { get; }
public CloudBlobClient CloudBlobClient { get; }
public CloudTableClient CloudTableClient { get; }
public async Task InitializeAsync();
}
あなたが抱えている問題、そして あなたが構築しているアプリケーション は、典型的ではありません。これは、次の2つの理由で非典型的です。
ただし、あなたの場合でも、解決策はかなりシンプルでエレガントです。
それを保持するクラスから初期化を抽出し、それを 構成ルート に移動します。その時点で、それらのクラスを作成して初期化しbeforeコンテナに登録し、それらの初期化されたクラスを登録の一部としてコンテナにフィードできます。
これは、特定の場合にうまく機能します。これは、(1回限りの)起動初期化を実行するためです。起動時の初期化は通常、コンテナーを構成する前に(または、完全に構成されたオブジェクトグラフが必要な場合は後で)実行されます。私が見たほとんどの場合、あなたの場合に効果的に行うことができるように、初期化は以前に行うことができます。
私が言ったように、あなたのケースは、標準と比較して、少し独特です。規範は次のとおりです。
通常、起動時の初期化を非同期で行うことの実際の利点はありません。起動時には、とにかく実行されるスレッドは1つだけであるため、実際のパフォーマンス上の利点はありません(これを並列化する場合もありますが、明らかに非同期は必要ありません)。また、一部のアプリケーションタイプは、synch-over-asyncの実行でデッドロックする可能性がありますが、コンポジションルートでは、使用しているアプリケーションタイプと、これが正確に問題があるかどうか。コンポジションルートは常にアプリケーション固有です。つまり、デッドロックのないアプリケーション(ASP.NET Core、Azure Functionsなど)のコンポジションルートで初期化を行う場合、通常、起動時の初期化を非同期で行うメリットはありません。
コンポジションルートでは、sync-over-asyncが問題であるかどうかがわかっているため、最初の使用時に同期的に初期化を行うこともできます。初期化の量は(要求ごとの初期化と比較して)有限であるため、必要に応じて、同期ブロッキングを使用してバックグラウンドスレッドで初期化を実行しても実際のパフォーマンスへの影響はありません。私たちがしなければならないのは、最初の使用時に初期化が行われることを保証するプロキシクラスをコンポジションルートに定義することです。これは、マーク・シーマンが答えとして提案したアイデアとほぼ同じです。
私はAzureFunctionsにまったく精通していなかったので、これは実際に非同期初期化を実際にサポートしていることを知っている最初のアプリケーションタイプ(もちろんコンソールアプリを除く)です。ほとんどのフレームワークタイプでは、ユーザーがこの起動初期化を非同期で行う方法はまったくありません。たとえば、ASP.NETアプリケーションまたはASP.NETCoreアプリケーションのStartup
クラスの_Application_Start
_イベント内にいる場合、非同期はありません。すべてが同期している必要があります。
その上、アプリケーションフレームワークでは、フレームワークのルートコンポーネントを非同期で構築することはできません。したがって、DIコンテナが非同期解決を行うという概念をサポートする場合でも、アプリケーションフレームワークのサポートが「不足」しているため、これは機能しません。 ASP.NETCoreのIControllerActivator
を例にとってみましょう。そのCreate(ControllerContext)
メソッドを使用すると、Controller
インスタンスを作成できますが、Create
メソッドの戻り値の型はobject
であり、_Task<object>
_ではありません。言い換えると、DIコンテナがResolveAsync
メソッドを提供する場合でも、ResolveAsync
呼び出しは同期フレームワークの抽象化の背後でラップされるため、ブロッキングが発生します。
ほとんどの場合、初期化はインスタンスごとまたは実行時に行われることがわかります。たとえば、SqlConnection
は通常、リクエストごとに開かれるため、各リクエストは独自の接続を開く必要があります。 「ジャストインタイム」で接続を開きたい場合、必然的にアプリケーションインターフェイスが非同期になります。ただし、ここでは注意してください。
同期の実装を作成する場合、決して別の実装が存在しないことが確実な場合にのみ、その抽象化を同期にする必要があります(または、非同期のプロキシ、デコレータ、インターセプタなど)。抽象化を無効に同期化した場合(つまり、_Task<T>
_を公開しないメソッドとプロパティがある場合)、Leaky Abstractionが手元にある可能性があります。これにより、後で非同期実装を取得するときに、アプリケーション全体で抜本的な変更を行う必要が生じる可能性があります。
言い換えると、asyncの導入により、アプリケーションの抽象化の設計にさらに注意を払う必要があります。これはあなたの場合にも当てはまります。今は起動時の初期化だけが必要かもしれませんが、定義した抽象化(およびAzureConnections
も)では、ジャストインタイムの非同期初期化が必要になることはありませんか? AzureConnections
の同期動作が実装の詳細である場合は、すぐに非同期にする必要があります。
これの別の例はあなたの INugetRepository です。そのメンバーは同期的ですが、それが同期的である理由はその実装が同期的であるため、それは明らかにリーク抽象化です。ただし、同期APIのみを持つレガシーNuGet NuGetパッケージを使用するため、その実装は同期的です。 INugetRepository
の実装は同期的ですが、完全に非同期である必要があることは明らかです。
非同期を適用するアプリケーションでは、ほとんどのアプリケーション抽象化にはほとんど非同期メンバーが含まれます。この場合、この種のジャストインタイム初期化ロジックも非同期にするのは簡単です。すべてがすでに非同期です。
要約する:
以下はあなたが探しているものではないと私はかなり確信していますが、なぜそれがあなたの質問に対処しないのか説明できますか?
public sealed class AzureConnections
{
private readonly Task<CloudStorageAccount> storage;
public AzureConnections()
{
this.storage = Task.Factory.StartNew(InitializeStorageAccount);
// Repeat for other cloud
}
private static CloudStorageAccount InitializeStorageAccount()
{
// Do any required initialization here...
return new CloudStorageAccount( /* Constructor arguments... */ );
}
public CloudStorageAccount CloudStorageAccount
{
get { return this.storage.Result; }
}
}
デザインを明確にするために、クラウドプロパティの1つだけを実装しましたが、他の2つも同様の方法で実装できました。
AzureConnections
コンストラクターは、さまざまなクラウドオブジェクトの初期化にかなりの時間がかかる場合でも、ブロックしません。
一方、作業は開始され、.NETタスクはpromiseのように動作するため、最初に値にアクセスしようとすると(Result
を使用)、InitializeStorageAccount
。
これはあなたが望んでいることではないという強い印象を受けますが、あなたが解決しようとしている問題がわからないので、少なくとも議論することがあるので、この答えを残したいと思いました。
プロキシシングルトンクラスで私がしていることをあなたがやろうとしているようです。
services.AddSingleton<IWebProxy>((sp) =>
{
//Notice the GetService outside the Task. It was locking when it was inside
var data = sp.GetService<IData>();
return Task.Run(async () =>
{
try
{
var credentials = await data.GetProxyCredentialsAsync();
if (credentials != null)
{
return new WebHookProxy(credentials);
}
else
{
return (IWebProxy)null;
}
}
catch(Exception ex)
{
throw;
}
}).Result; //Back to sync
});