又再一次被ODP.NET的版號問題搞得頭昏腦脹,某顆共用元件Blah.dll參考了ODP.NET 11.2,使用組件繫結重新導向(Binding Redirect)的技巧,在一些僅有ODP.NET 9.2的網站及開發機器上運作順暢,但相同做法在只有ODP.NET 10.2的機器上卻不管用,冒出"... has a higher version than referenced assembly ..."錯誤。而依過去的經驗,對繫結重新導向始終存在"有時可以,有時不管用"的混亂印象。

歸納不出結論,看似有鬼,其實是對底層原理不夠了解。下定決心,這回非把它查個水落石出不可。

我模擬的情境如下: 有一顆共用元件Boo.dll,包含類別BooClass,內含一個靜態函數GetVersion(),編譯兩次產生兩個版本分別為1.0及2.0:
(註: Boo.dll專案必須加上數位簽章,並需修改AssemblyInfo.cs [assembly: AssemblyVersion("")]設定為1.0.0.0及2.0.0.0,以便稍後繫結重新導向時指定publicKeyToken及版號之用)

using System;
using System.Collections.Generic;
using System.Text;
 
namespace Boo
{
    public class BooClass
    {
        public static string GetVersion()
        {
            return "1.0";
        }
    }
}

元件Bar.dll參考Boo.dll,內有BarClass提供GetBooVersion()靜態方法呼叫BooClass.GetVersion()取得字串。因為Boo.dll有1.0及2.0之分,依照參考Boo.dll版本不同,Bar.dll也分別編譯出1.0及2.0兩個版本:

using System;
using Boo;
 
namespace Bar
{
    public class BarClass
    {
        public static string GetBooVersion()
        {
            return BooClass.GetVersion();
        }
    }
}

元件Foo.dll也參考Boo.dll,提供GetBoo()及GetBooVersion()兩個靜態方法,同樣地,依照參考Boo.dll版本不同,分別編譯成1.0及2.0兩個版本:

using System;
using Boo;
 
namespace Foo
{
    public class FooClass
    {
        public static BooClass GetBoo()
        {
            return new BooClass();
        }
 
        public static string GetBooVersion()
        {
            return BooClass.GetVersion();
        }
    }
}

故這個搞死人的版號混亂系統共有六顆不同的元件:

  • Boo.dll [1.0]
  • Boo.dll [2.0]
  • Foo.dll [1.0] (參考Boo 1.0)
  • Foo.dll [2.0] (參考Boo 2.0)
  • Bar.dll [1.0] (參考Boo 1.0)
  • Bar.dll [2.0] (參考Boo 2.0)

現有網站BooWeb,test1.aspx涉及BooClass及BarClass,程式邏輯如下:

<%@ Page Language="C#" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        Response.Write("<li>BooTest = " + Boo.BooClass.GetVersion());
        Response.Write("<li>BarTest = " + Bar.BarClass.GetBooVersion());
        Response.End();        
    }
</script>

另有test2.aspx邏輯如下,差別在於使用了BooClass及FooClass:

<%@ Page Language="C#" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        Response.Write("<li>BooTest = " + Boo.BooClass.GetVersion());
        Response.Write("<li>FooTest = " + Foo.FooClass.GetBoo());
        Response.End();        
    }
</script>

我定義了四種測試組合,在繼續看下去之前,大家不妨試著回答,看看哪些組合可行哪些不行,考驗自己對繫結導向的了解深度:

組合1: test1.aspx + Bar.dll 1.0 + Boo.dll 2.0
組合2: test1.aspx + Bar.dll 2.0 + Boo.dll 1.0
組合3: test2.aspx + Foo.dll 1.0 + Boo.dll 2.0
組合4: test2.aspx + Foo.dll 2.0 + Boo.dll 1.0

答案是組合1,2,3可行,組合4不行。你答對了嗎?

以下是實測結果:

組合1: Bar.dll 1.0 + Boo.dll 2.0

Bar.dll 1.0預設要參照Boo.dll 1.0,但bin下只有Boo.dll 2.0,故執行時出現"Could not load file or assembly 'BooClass, Version=1.0.0.0, Culture=neutral, PublicKeyToken=98288d8586fbe8ef' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) "錯誤,在web.config加入bindingRedirect設定

<dependentAssembly>
    <assemblyIdentity name="BooClass" publicKeyToken="98288D8586FBE8EF"/>
    <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>
</dependentAssembly>

強迫Bar.dll 1.0改用Boo.dll 2.0,即可順利顯示"BooTest = 2.0, BarTest = 2.0"

組合2: Bar.dll 2.0 + Boo.dll 1.0

跟組合1狀況相仿,執行時出現"Could not load file or assembly 'BooClass, Version=2.0.0.0, Culture=neutral, PublicKeyToken=98288d8586fbe8ef' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) ",加入<bindingRedirect oldVersion="2.0.0.0" newVersion="1.0.0.0"/>強迫Bar.dll 2.0改用Boo.dll 1.0,順利顯示"BooTest = 1.0, BarTest = 1.0"

組合3: Foo.dll 1.0 + Boo.dll 2.0

跟組合1的狀況相仿,執行時出現"Could not load file or assembly 'BooClass, Version=1.0.0.0, Culture=neutral, PublicKeyToken=98288d8586fbe8ef' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) "錯誤,加入<bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>後可強迫Foo.dll 1.0改用Boo.dll 2.0,順利顯示"BooTest = 2.0, FooTest = Boo.BooClass"

組合4: Foo.dll 2.0 + Boo.dll 1.0

狀況與1,2,3不同,完全無法執行,編譯階段就發生錯誤"CS1705: Assembly 'FooClass, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null' uses 'BooClass, Version=2.0.0.0, Culture=neutral, PublicKeyToken=98288d8586fbe8ef' which has a higher version than referenced assembly 'BooClass, Version=1.0.0.0, Culture=neutral, PublicKeyToken=98288d8586fbe8ef' ",加入<bindingRedirect oldVersion="2.0.0.0" newVersion="1.0.0.0"/>亦無效!

【結論】

使用bindingRedirect時,並不存在newVersion要高於oldVersion的限制,在組合2中BarClass 2.0可以改用BooClass 1.0即為實證。

會受版號高低影響的情境如下: 在使用FooClass的場合,當呼叫端或另一顆元件使用了BooClass(版本A),而FooClass在公開屬性、方法也使用到BooClass(版本B)。若版本B的版號低於版本A,執行時會出現FooClass無法找到BooClass(版本B)的錯誤,此時可透過bindingRedirect解決[即組合3的情境];若版本B的版號高於版本A,將引發CS1705編譯錯誤,且無法使用bindingRedirect解決[即組合4的情境]。

組合2 BarClass只所以不受版號由高變低限制,是因為其在公開介面中沒有用到BooClass。若我們試著在BarClass中加入private BooClass MyBoo { get; set; }屬性,測試仍可成功;但若改成pubic BooClass MyBoo { get; set; },便會出現跟組合4一樣的編譯錯誤。

最後一道謎是為何我的共用元件參考了ODP.NET 11.2,面對新版本導向舊版本的情境,使用bindingRedirect在ODP.NET 9.2環境OK,在ODP.NET 10.2就不行?? 套用前述結論,關鍵在於ODP.NET曾砍掉重練的版號排法,Oracle.DataAccess.dll在9.2的版號是9.2.*,10.1還是10.1.*,10.2起變成2.102.*,11.2的版號是2.112.*,因此ODP.NET 11.2導向9.2是2.112->9.2,讓.NET誤以為是換到版號更高的新版本,搞出美麗的錯誤。

在理解這些之後,之前無法捉摸的鬼問題,頓時清晰無比,一切符合邏輯。豁然開朗的感覺,總讓人心情澎湃,真才是扮演茶包射手最大的樂趣!


Comments

# by imiss

hi~ 試了一下,組合一的 bindingRedirect 設定應該有誤, 改成底下的就可以了:) <dependentAssembly> <assemblyIdentity name="BooClass" publicKeyToken="98288D8586FBE8EF"/> <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/> </dependentAssembly>

# by imiss

<%@ Page Language="C#" %> <script runat="server"> void Page_Load(object sender, EventArgs e) { Response.Write("<li>BooTest = " + Boo.BooClass.GetVersion()); Response.Write("<li>FooTest = " + Foo.FooClass.GetBoo()); Response.End(); } </script> 網頁中的 Response.Write("<li>FooTest = " + Foo.FooClass.GetBoo()); 應改為 Response.Write("<li>FooTest = " + Foo.FooClass.GetBooVersion()); 如果有錯麻煩黑大指正^^"

# by Jeffrey

to imss, 感謝你的細心指正,組合1的oldVersion與newVersion真的寫反了(羞),已改正。 至於FooClass,由於它有public Boo GetBoo()存在,因此不管寫GetBoo()或寫GetBooVersion()都應導致CS1705編譯錯誤,文中寫GetBoo()旨在突顯它傳回Boo,強調其公開介面涉及了Boo類別。

# by imiss

明白了,謝謝:D

Post a comment