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の本体コードをサンプルとして取り出しただけですので、本体コードを参照したい場合はこちらを参照ください。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace LazyLoadingAssemblySample
{
class Program
{
/// <summary>
/// 遅延ローディング対象のDLLが格納されたパス
/// </summary>
private const string c_DirectryPath = @".\ImportDLLs\ClassLibrarySample.dll";
/// <summary>
/// 登録されたAssemblyのリスト
/// </summary>
private static List<AssemblyInfo> s_RegisteredAssemblies = new List<AssemblyInfo>();
static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
// Assemblyの遅延ローディング対象となるリストの一覧を読み込む
LoadAssemblyFrom(@"file://" + Path.GetFullPath(c_DirectryPath));
// インスタンスの生成を試みる
try
{
var type = AppDomain.CurrentDomain.CreateInstance("ClassLibrarySample, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "ClassLibrarySample.SampleClass");
if (type != null)
{
Console.WriteLine(type.Unwrap().ToString());
}
}
catch (Exception e)
{
// 何もしない
}
AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
}
/// <summary>
/// AppDomain.CurrentDomain.CreateInstanceで失敗すると本イベントハンドラが呼び出される
/// 登録したAssemblyから一致するものを読み込む
/// </summary>
private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
{
AssemblyName assemblyName = new AssemblyName(args.Name);
AssemblyInfo assemblyInfo = s_RegisteredAssemblies.FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.AssemblyName));
if (assemblyInfo != null)
{
if (assemblyInfo.Assembly == null)
{
assemblyInfo.Assembly = Assembly.LoadFrom(assemblyInfo.AssemblyUri.LocalPath);
}
return assemblyInfo.Assembly;
}
return null;
}
/// <summary>
/// 指定されたファイルのAssemblyの名前とパスを読み込む
/// 実体のAssemblyは読み込まない
/// </summary>
private static void LoadAssemblyFrom(string assemblyFilePath)
{
Uri assemblyUri = GetFileUri(assemblyFilePath);
if (assemblyUri == null)
{
throw new ArgumentException("InvalidArgumentAssemblyUri", nameof(assemblyFilePath));
}
if (!File.Exists(assemblyUri.LocalPath))
{
throw new FileNotFoundException(null, assemblyUri.LocalPath);
}
AssemblyName assemblyName = AssemblyName.GetAssemblyName(assemblyUri.LocalPath);
AssemblyInfo assemblyInfo = s_RegisteredAssemblies.FirstOrDefault(a => assemblyName == a.AssemblyName);
if (assemblyInfo != null)
{
return;
}
assemblyInfo = new AssemblyInfo() { AssemblyName = assemblyName, AssemblyUri = assemblyUri };
s_RegisteredAssemblies.Add(assemblyInfo);
}
/// <summary>
/// 指定されたファイルパスからUriを生成する
/// </summary>
private static Uri GetFileUri(string filePath)
{
if (String.IsNullOrEmpty(filePath))
{
return null;
}
Uri uri;
if (!Uri.TryCreate(filePath, UriKind.Absolute, out uri))
{
return null;
}
if (!uri.IsFile)
{
return null;
}
return uri;
}
}
}
以下は上記で読み込むダミーのクラスです。
// 別ソリューションで作成します。
// Assemblyが読み込めることが確認できれば良いので中身は空っぽです。
namespace ClassLibrarySample
{
public class SampleClass
{
}
}
おわりに
オープンソースであるPrismフレームワークから、Assemblyの遅延ローディングについて学ぶことができました。
活用どころがあるかはわかりませんが、こういったテクニックを利用することでフレームワークの下支えになっていると学べました。
引き続き、当初の不明点を解消できるように学習を進めたいと思います。