2019年10月1日 星期二

[超譯] 解決 .Net Reference 的版本衝突

## 前言

最近遇到一個狀況,我包好的 dll 會用到 Newtonsoft.Json.dll (Json.Net),版本是 4.5。拿去給別人(A公司)用,他們也用了別人(B公司)的 dll,也用到 Newtonsoft.Json.dll (Json.Net),但是他們指定的是 9.0.0。當 Visual Studio 把我指定的 Newtonsoft.Json.dll 搬到執行檔旁邊,B公司的 dll 就無法啟動。當換成 B公司指定的,就換我的 dll 無法啟動。A公司的人大叫該怎麼辦?因為我們公司比較小,所以我要解決這個問題。

## 主旨

一般的解決方法都是叫大家重新抓過新版的 dll,像我們這次就是叫我這比較舊的版本要升級。但,支援的 .Net Framework 不能超過 4.5.2,超過會讓其他家的 dll 無法運作。這作法也沒什麼不好,只是沒有一勞永逸。要是將來我的 dll 給了幾家客戶,他們幾家要的 Newtonsoft.Json.dll 都不一樣,或是他們升級了,我這小公司就得配合找到對應的 Newtonsoft.Json.dll 來 rebuild 才行。

網路上用不同關鍵字找過,我認為這個比較好。https://michaelscodingspot.com/how-to-resolve-net-reference-and-nuget-package-version-conflicts/

## 超譯

重點,解法有兩個方向,四個方法。

兩個方向是 (1) 如果可以,全部改成同一個版本。 (2) 使用 dll side-by-side loading。

四個解法之中,作者建議解法(1)

### 解法(1) Use a single assembly version with Binding Redirect

在有 app.config 或 dll.config 的存在時,增加一個 runtime 標籤。舉例:

```
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
    </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="1.2.11" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>
```
作者舉 log4net 為例,加上 runtime 標籤指示 CLR 使用 1.2.11 版的 log4net。

### 解法(2) Override AssemblyResolve for side-by-side loading (No need for strong names)

使用 AppDomain.CurrentDomain.AssemblyResolve 事件來處理不同版本的尋找問題。

首先將要處理的 dll 的 CopyLocal 設成 False。並將不同版本的 dll 放到不同磁碟位置。然後在執行檔的最開頭註冊 AssemblyResolve 事件。它在 app 找不到 dll 的時候會被呼叫,然後我們就寫好程式來處理它。

範例:

```
static void Main(string[] args)
{
    AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
    {
        string assemblyInfo = resolveArgs.Name;// e.g "Lib1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
        var parts = assemblyInfo.Split(',');
        string name = parts[0];
        var version = Version.Parse(parts[1].Split('=')[1]);
        string fullName;
        if (name == "Lib1" && version.Major == 1 && version.Minor == 0)
        {
            fullName = new FileInfo(@"V10\Lib1.dll").FullName;
        }
        else if (name == "Lib1" && version.Major == 1 && version.Minor == 1)
        {
            fullName = new FileInfo(@"V11\Lib1.dll").FullName;
        }
        else
        {
            return null;
        }

        return Assembly.LoadFile(fullName);
    };
```

### 解法(3) Copy assemblies to different folders and use <codebase> to load them side-by-side (Requires strong names)

這個方法需要 dll 有強式名字。這點 Newtonsoft.Json.dll 或 log4net 這類公開的 dll 做得到。或是自己做的 dll 也來個強式名字也行。將要處理的 dll 的 CopyLocal 設成 False。

舉例:

```
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
        <dependentAssembly>
          <assemblyIdentity name="StrongNameLib" culture="neutral" publicKeyToken="6a770f8bdf3d476a" />
          <codeBase version="1.0.0.0" href="StrongNameLibV10/StrongNameLib.dll"/>
          <codeBase version="1.1.0.0" href="StrongNameLibV11/StrongNameLib.dll"/>
        </dependentAssembly>
      </assemblyBinding>
  </runtime>
</configuration>
```

這就是告訴 CLR 按照版本去找 dll。如果沒有強式名字,CLR 會忽略版本,所以總是找到第一個 codeBase 所列的 dll。href 也可以用絕對路徑,像是 @"FILE://c:/Lib/MyLib.dll"

### 解法(4) Install assemblies to the Global Assembly Cache (GAC)

這個方法是把要用到的 dll 註冊到 GAC (Global Assembly Cache) 裡。這會需要 dll 有強式名字,而且還要使用者手動執行指令(或是用安裝程式)將 dll 註冊到 GAC 裡。例如:

```
gacutil.exe -i "c:\Dev\Project\Debug\MyLib.dll"
```

將要處理的 dll 的 CopyLocal 設成 False。依照 runtime 的尋找規則,會先找尋 GAC 有沒有可用 dll,所以因為 GAC 都有註冊,所以各自會找到對應的版本來使用。

這個方法會需要使用者手動註冊或者使用 installer 將 dll 註冊到 GAC。也就這樣而已。但是有些人就是會嫌……。