web-dev-qa-db-ja.com

AppDomainにデプロイされたすべてのアセンブリをプリロードする方法

PDATE:私は今、それではるかに満足している解決策を持っています、私が尋ねるすべての問題を解決するわけではありませんが、それはそうするための道を明確にします。これを反映するために自分の答えを更新しました。

元の質問

アプリドメインを指定すると、Fusion(.Netアセンブリローダー)が特定のアセンブリをプローブするさまざまな場所があります。明らかに、この機能は当然のことと考えており、プロービングは.Netランタイム内に埋め込まれているように見えるため(Assembly._nLoad内部メソッドはReflect-Loadingのエントリポイントのようです-暗黙の読み込みはおそらく同じ基礎となるアルゴリズムでカバーされていると思います)、開発者はこれらの検索パスにアクセスできないようです。

私の問題は、多くの動的な型解決を行うコンポーネントがあり、特定のAppDomainに対してユーザーがデプロイしたすべてのアセンブリが、作業を開始する前にプリロードされていることを確認できる必要があることです。はい、起動が遅くなりますが、このコンポーネントから得られるメリットは、これを完全に上回ります。

私がすでに書いた基本的なロードアルゴリズムは次のとおりです。フォルダーのセットをディープスキャンして.dllを探し(.exeは現在除外されています)、Assembly.LoadFromを使用してdllをロードしますすでにAppDomainにロードされているアセンブリのセットにAssemblyNameが見つからない場合(これは非効率的に実装されますが、後で最適化できます)。

void PreLoad(IEnumerable<string> paths)
{
  foreach(path p in paths)
  {
    PreLoad(p);
  }
}

void PreLoad(string p)
{
  //all try/catch blocks are elided for brevity
  string[] files = null;

  files = Directory.GetFiles(p, "*.dll", SearchOption.AllDirectories);

  AssemblyName a = null;
  foreach (var s in files)
  {
    a = AssemblyName.GetAssemblyName(s);
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(
        Assembly => AssemblyName.ReferenceMatchesDefinition(
        Assembly.GetName(), a)))
      Assembly.LoadFrom(s);
  }    
}

LoadFromを使用するのは、Load()を使用すると、Fusionがプローブしたときに、予期した場所からロードされたアセンブリが見つからない場合に、Fusionによって重複するアセンブリがロードされる可能性があることがわかったためです。

したがって、これが適切に行われているので、Fusionがアセンブリを検索するときに使用する検索パスの優先順位(最高から最低)のリストを取得するだけです。次に、それらを単純に繰り返すことができます。

GACはこれとは無関係であり、Fusionが使用する可能性のある環境駆動型の固定パスには関心がありません。アプリ用に明示的にデプロイされたアセンブリを含むAppDomainから収集できるパスのみです。

これの最初の反復では、単にAppDomain.BaseDirectoryを使用しました。これは、サービス、フォームアプリ、コンソールアプリで機能します。

ただし、Asp.Net Webサイトでは機能しません。これは、AppDomain.DynamicDirectory(Asp.Netが動的に生成されたページクラスとAspxページコードが参照するアセンブリを配置する場所)と、少なくとも2つの主要な場所があるためです。次に、サイトのBinフォルダー-AppDomain.SetupInformation.PrivateBinPathプロパティから検出できます。

これで、最も基本的なタイプのアプリのコードが機能するようになりました(SQL ServerでホストされるAppDomainは、ファイルシステムが仮想化されているため、別の話です)-しかし、数日前に、このコードが単に機能しないという興味深い問題に遭遇しました。 :nUnitテストランナー。

これは両方のシャドウコピーを使用し(したがって、私のアルゴリズムは、binフォルダーからではなく、シャドウコピードロップフォルダーからそれらを検出してロードする必要があります)、PrivateBinPathをベースディレクトリを基準として設定します。

そしてもちろん、私がおそらく考慮しなかった他のホスティングシナリオがたくさんあります。ただし、これは有効である必要があります。有効でない場合、Fusionはアセンブリのロード時にチョークします。

これらの新しいシナリオが発生したときに対応するために、周りを感じてハックを導入するのをやめたいと思います-AppDomainとそのセットアップ情報が与えられた場合、選択するためにスキャンする必要があるフォルダーのリストを生成する機能が必要ですロードされるすべてのDLLをアップします。 AppDomainの設定方法に関係なく。 Fusionがそれらをすべて同じものとして見ることができるなら、私のコードもそうすべきです。

もちろん、.Netがその内部を変更した場合、アルゴリズムを変更する必要があるかもしれません-それは私が耐えなければならない単なるクロスです。同様に、SQL Serverやその他の同様の環境を、現時点ではサポートされていないエッジケースと見なすことができてうれしいです。

何か案は!?

29
Andras Zoltan

プライベートビンパスがまだ正しく処理されていないことを除けば、最終的な解決策にはるかに近いものを得ることができました。以前のライブコードをこれに置き換え、バーゲンで発生したいくつかの厄介なランタイムバグも解決しました(非常に多くのdllを参照するC#コードの動的コンパイル)。

それ以来私が発見した黄金律は 常にロードコンテキストを使用する であり、LoadFromコンテキストではありません。これは、ロードコンテキストが常に最初に表示されるためです。したがって、LoadFromコンテキストを使用する場合、自然にバインドされるのと同じ場所から実際にロードした場合にのみヒットします。これは必ずしも簡単ではありません。

このソリューションは、binフォルダーと「標準」アプリの違いを考慮して、Webアプリケーションの両方で機能します。 PrivateBinPathの問題に対応するために簡単に拡張できます。正確に読み取られる方法について信頼できるハンドルを取得できれば(!)

_private static IEnumerable<string> GetBinFolders()
{
  //TODO: The AppDomain.CurrentDomain.BaseDirectory usage is not correct in 
  //some cases. Need to consider PrivateBinPath too
  List<string> toReturn = new List<string>();
  //slightly dirty - needs reference to System.Web.  Could always do it really
  //nasty instead and bind the property by reflection!
  if (HttpContext.Current != null)
  {
    toReturn.Add(HttpRuntime.BinDirectory);
  }
  else
  {
    //TODO: as before, this is where the PBP would be handled.
    toReturn.Add(AppDomain.CurrentDomain.BaseDirectory);
  }

  return toReturn;
}

private static void PreLoadDeployedAssemblies()
{
  foreach(var path in GetBinFolders())
  {
    PreLoadAssembliesFromPath(path);
  }
}

private static void PreLoadAssembliesFromPath(string p)
{
  //S.O. NOTE: ELIDED - ALL EXCEPTION HANDLING FOR BREVITY

  //get all .dll files from the specified path and load the lot
  FileInfo[] files = null;
  //you might not want recursion - handy for localised assemblies 
  //though especially.
  files = new DirectoryInfo(p).GetFiles("*.dll", 
      SearchOption.AllDirectories);

  AssemblyName a = null;
  string s = null;
  foreach (var fi in files)
  {
    s = fi.FullName;
    //now get the name of the Assembly you've found, without loading it
    //though (assuming .Net 2+ of course).
    a = AssemblyName.GetAssemblyName(s);
    //sanity check - make sure we don't already have an Assembly loaded
    //that, if this Assembly name was passed to the loaded, would actually
    //be resolved as that Assembly.  Might be unnecessary - but makes me
    //happy :)
    if (!AppDomain.CurrentDomain.GetAssemblies().Any(Assembly => 
      AssemblyName.ReferenceMatchesDefinition(a, Assembly.GetName())))
    {
      //crucial - USE THE Assembly NAME.
      //in a web app, this Assembly will automatically be bound from the 
      //Asp.Net Temporary folder from where the site actually runs.
      Assembly.Load(a);
    }
  }
}
_

まず、選択した「アプリフォルダー」を取得するために使用するメソッドがあります。これらは、ユーザーがデプロイしたアセンブリがデプロイされる場所です。 PrivateBinPath Edgeの場合(一連の場所である可能性があります)のため、これはIEnumerableですが、実際には、現時点では1つのフォルダーのみです。

次のメソッドはPreLoadDeployedAssemblies()です。これは、何かを行う前に呼び出されます(ここでは、_private static_としてリストされています-私のコードでは、これは、常にこれをトリガーするパブリックエンドポイントを持つはるかに大きな静的クラスから取得されます初めて何かをする前に実行されるコード。

最後に、肉と骨があります。ここで最も重要なことは、アセンブリファイルを取得し、アセンブリ名を取得することです。 Assembly.Load(AssemblyName)に渡し、LoadFromを使用しないでください。

以前は、LoadFromの方が信頼性が高く、Webアプリで一時的なAsp.Netフォルダーを手動で見つけなければならないと思っていました。あなたはしません。必ずロードする必要があることがわかっているアセンブリの名前を知って、それを_Assembly.Load_に渡すだけです。結局のところ、それは実際には.Netの参照読み込みルーチンが行うことです:)

同様に、このアプローチは、_AppDomain.AssemblyResolve_イベントをハングアップすることによって実装されたカスタムアセンブリプロービングでもうまく機能します。アプリのbinフォルダーを、スキャンされるように、プラグインコンテナーフォルダーに拡張します。通常のプロービングが失敗したときに確実にロードされるように、とにかくAssemblyResolveイベントをすでに処理している可能性があるため、すべてが以前と同じように機能します。

19
Andras Zoltan

これが私がすることです:

public void PreLoad()
{
    this.AssembliesFromApplicationBaseDirectory();
}

void AssembliesFromApplicationBaseDirectory()
{
    string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
    this.AssembliesFromPath(baseDirectory);

    string privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath;
    if (Directory.Exists(privateBinPath))
        this.AssembliesFromPath(privateBinPath);
}

void AssembliesFromPath(string path)
{
    var assemblyFiles = Directory.GetFiles(path)
        .Where(file => Path.GetExtension(file).Equals(".dll", StringComparison.OrdinalIgnoreCase));

    foreach (var assemblyFile in assemblyFiles)
    {
        // TODO: check it isnt already loaded in the app domain
        Assembly.LoadFrom(assemblyFile);
    }
}
4
Andrew Bullock

Assembly.GetExecutingAssembly()。Locationを確認してみましたか?これにより、コードが実行されているアセンブリへのパスが得られます。 NUnitの場合、アセンブリがシャドウコピーされた場所になると思います。

0
Andy