AppContextを利用してAssemblyを遅延ローディングする
はじめに
以下が気になり、色々ソースコードを追ってみた結果の副産物になります。
https://qiita.com/hiki_neet_p/items/e04b5ac692aa18df0968
ここで、何もしていないのにMainWindowがDIコンテナに登録されているのがポイントです。
同じアセンブリ(プロジェクト)内のViewとViewModelが自動的に登録されるのです。
Prismの便利さの本質はここに集約されるのではないでしょうか。
実際にどのようなロジックになっているかは、以下のUTを実行してみるとわかります。
https://github.com/PrismLibrary/Prism/blob/master/tests/Wpf/Prism.Wpf.Tests/Modularity/AssemblyResolverFixture.Desktop.cs#L71
本記事では、上記の実装を単純化したサンプルコードで紹介します。
※ 以下の通り、C#では元々Assemblyの読み込みは遅延で行われるため、自分で実装する必要はありません。ただし、フレームワークとして凝った実装をしたい場合には必要なテクニックなのだと思われます。
https://docs.microsoft.com/ja-jp/cpp/build/reference/linker-support-for-delay-loaded-dlls?view=vs-2019
サンプル
以下、サンプルコードです。指定したパス以下のAssemblyInfoを収集し、Type.CreateInstanceします。失敗すると、イベントハンドラでAssemblyInfoで名前が一致するものを探し、実際にAssemblyを読み込みます。
Prismの本体コードをサンプルとして取り出しただけですので、本体コードを参照したい場合はこちらを参照ください。
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Linq;usingSystem.Reflection;namespaceLazyLoadingAssemblySample{classProgram{/// <summary>/// 遅延ローディング対象のDLLが格納されたパス/// </summary>privateconststringc_DirectryPath=@".\ImportDLLs\ClassLibrarySample.dll";/// <summary>/// 登録されたAssemblyのリスト/// </summary>privatestaticList<AssemblyInfo>s_RegisteredAssemblies=newList<AssemblyInfo>();staticvoidMain(string[]args){AppDomain.CurrentDomain.AssemblyResolve+=newResolveEventHandler(CurrentDomain_AssemblyResolve);// Assemblyの遅延ローディング対象となるリストの一覧を読み込むLoadAssemblyFrom(@"file://"+Path.GetFullPath(c_DirectryPath));// インスタンスの生成を試みるtry{vartype=AppDomain.CurrentDomain.CreateInstance("ClassLibrarySample, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null","ClassLibrarySample.SampleClass");if(type!=null){Console.WriteLine(type.Unwrap().ToString());}}catch(Exceptione){// 何もしない}AppDomain.CurrentDomain.AssemblyResolve-=CurrentDomain_AssemblyResolve;}/// <summary>/// AppDomain.CurrentDomain.CreateInstanceで失敗すると本イベントハンドラが呼び出される/// 登録したAssemblyから一致するものを読み込む/// </summary>privatestaticAssembly?CurrentDomain_AssemblyResolve(object?sender,ResolveEventArgsargs){AssemblyNameassemblyName=newAssemblyName(args.Name);AssemblyInfoassemblyInfo=s_RegisteredAssemblies.FirstOrDefault(a=>AssemblyName.ReferenceMatchesDefinition(assemblyName,a.AssemblyName));if(assemblyInfo!=null){if(assemblyInfo.Assembly==null){assemblyInfo.Assembly=Assembly.LoadFrom(assemblyInfo.AssemblyUri.LocalPath);}returnassemblyInfo.Assembly;}returnnull;}/// <summary>/// 指定されたファイルのAssemblyの名前とパスを読み込む/// 実体のAssemblyは読み込まない/// </summary>privatestaticvoidLoadAssemblyFrom(stringassemblyFilePath){UriassemblyUri=GetFileUri(assemblyFilePath);if(assemblyUri==null){thrownewArgumentException("InvalidArgumentAssemblyUri",nameof(assemblyFilePath));}if(!File.Exists(assemblyUri.LocalPath)){thrownewFileNotFoundException(null,assemblyUri.LocalPath);}AssemblyNameassemblyName=AssemblyName.GetAssemblyName(assemblyUri.LocalPath);AssemblyInfoassemblyInfo=s_RegisteredAssemblies.FirstOrDefault(a=>assemblyName==a.AssemblyName);if(assemblyInfo!=null){return;}assemblyInfo=newAssemblyInfo(){AssemblyName=assemblyName,AssemblyUri=assemblyUri};s_RegisteredAssemblies.Add(assemblyInfo);}/// <summary>/// 指定されたファイルパスからUriを生成する/// </summary>privatestaticUriGetFileUri(stringfilePath){if(String.IsNullOrEmpty(filePath)){returnnull;}Uriuri;if(!Uri.TryCreate(filePath,UriKind.Absolute,outuri)){returnnull;}if(!uri.IsFile){returnnull;}returnuri;}}}
以下は上記で読み込むダミーのクラスです。
// 別ソリューションで作成します。// Assemblyが読み込めることが確認できれば良いので中身は空っぽです。namespaceClassLibrarySample{publicclassSampleClass{}}
おわりに
オープンソースであるPrismフレームワークから、Assemblyの遅延ローディングについて学ぶことができました。
活用どころがあるかはわかりませんが、こういったテクニックを利用することでフレームワークの下支えになっていると学べました。
引き続き、当初の不明点を解消できるように学習を進めたいと思います。