組件繫結重新導向鬼問題-版號高低之謎
4 |
又再一次被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