銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

Visual Studio SDK でも ReactivePropertyを使いたい (bindingRedirect編)

Visual Studio SDKを使ってVisual Studio拡張を使っているのですが、UI部分をWPFで作成していて、じゃあReactivePropertyを使おうと思ったらこんなエラーが出ました。

f:id:tanaka733:20180129011635p:plain

というわけでこれに対処したいと思います。

勘のいい方だとすぐに気付くと思うのですが、NuGetできちんと依存関係解決しているのに、依存関係が見つからないということなので、bindingRedirectの問題です。実際にプロジェクトで参照しているDLLのバージョンを確認するうと、確かにパッチレベルが違う値になっています。

f:id:tanaka733:20180129175350p:plain

プロジェクトに追加されたファイルをよく見ると、app.configに次のような値が追加されています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Reactive.Core" publicKeyToken="94bc3704cddfc263" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.3000.0" newVersion="3.0.3000.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Reactive.Interfaces" publicKeyToken="94bc3704cddfc263" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1000.0" newVersion="3.0.1000.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

というわけで、app.configに追加されているbindingRedirectが実際にはうまく解決されていないという問題になります。なぜかというと、Visual Studio SDKの場合、プロセス自体はdevenv.exeなのでこのプロセス起動時に渡すapp.configにこのbindingRedirectの設定が記載されていないといけないのに、Visual Studio SDKのプロジェクトのapp.configに記載があっても解決してくれないせいです。かといって、devenvはユーザー(あるいはマシン)グローバルな設定なので、Visual Studio 拡張の一パッケージが変更すべき値ではなさそうです。というわけでどうしようか悩んでいたらこんな記事を見つけました。

Redirecting Assembly Loads at Runtime – SLaks.Blog

AppDomain.CurrentDomain.AssemblyResolveイベントを登録することで、アセンブリの解決を実行時にリダイレクトしてやる方法です。このイベントに関する詳細はこちらを参照してください。

AppDomain.AssemblyResolve イベント (System)

というわけで、このコードを拝借してちょっと変えたものを、Visual Studio SDKのパッケージクラスのstaticコンストラクタから実行することにしました。

using System;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.Win32;

namespace MyVisualStudio.Vsix
{

    //この辺の属性は自動生成のままいじっていない
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
    [Guid(MyVSPackage .PackageGuidString)]
    [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
    [ProvideMenuResource("Menus.ctmenu", 1)]
    [ProvideToolWindow(typeof(MyVisualStudio.Vsix.Views.MyProjectWindow))]
    public sealed class MyVSPackage : Package
    {
        //VS拡張に必要なコードは省略

        static MyVSPackage()
        {
            RedirectAssembly("System.Reactive.Core", new Version(3, 0, 3000, 0), "94bc3704cddfc263");
            RedirectAssembly("System.Reactive.Interfaces", new Version(3, 0, 1000, 0), "94bc3704cddfc263");
        }

        public static void RedirectAssembly(string shortName, Version targetVersion, string publicKeyToken)
        {
            ResolveEventHandler handler = null;

            handler = (sender, args) => {
                var requestedAssembly = new AssemblyName(args.Name);
                if (requestedAssembly.Name != shortName)
                    return null;

                Debug.WriteLine($"Redirecting assembly load of {args.Name}, loaded by {args.RequestingAssembly?.FullName ?? "(unknown)"}");
                
                if (requestedAssembly.Version > targetVersion)
                {
                    Debug.WriteLine($"Request assemby's version {requestedAssembly.Version} is higher than the target version {targetVersion}. Stop to load the target assembly.");
                    return null;
                }
                requestedAssembly.Version = targetVersion;
                requestedAssembly.SetPublicKeyToken(new AssemblyName($"x, PublicKeyToken={publicKeyToken}").GetPublicKeyToken());
                requestedAssembly.CultureInfo = CultureInfo.InvariantCulture;

                AppDomain.CurrentDomain.AssemblyResolve -= handler;

                return Assembly.Load(requestedAssembly);
            };
            AppDomain.CurrentDomain.AssemblyResolve += handler;
        }
    }
}

一応、ターゲットとして読み込むバージョンより大きなバージョンがリクエストされた場合は読み込まないコードを追加しています。なお、一度ロードに失敗したアセンブリは結果がキャッシュされて実行中に再度読み込みが試行されることはないので、もし同じアセンブリを読み込もうとして失敗するVS拡張が別にインストールされていて、そちらが先にアセンブリの読み込みを実行する場合は、このコードは効果がないと思われます。

なお、bindingRedirectが発生してしまう問題はReactiveExtension側の問題で、次のバージョン(いつだ?)で解消される予定になっています。

github.com