有個需求,想在Web Service中傳遞Dictionary<string, string>參數,例如:

[WebMethod]
public Dictionary<string, string> Process(Dictionary<string, string> dct)
{
    //Do something on the Dictionary
    //... blah blah blah ....
 
    return dct;
}

天不從人願,以上的寫法會產生Web Service不支援IDictionary的錯誤:

The type System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]] is not supported because it implements IDictionary.

既是IDictionary的原罪,就算是換用Hashtable、ListDictionary應該也是同樣結果,測試之下果然無一倖免。

Google發現討論區有篇來自MS RD的留言,算是證實這是先天限制:

ASMX web services do not support types that implement IDictionary since V1. This is by design and was initially done since key-value constraints could not be appropriately expressed in schema.
來源: http://social.msdn.microsoft.com/Forums/en-US/asmxandxml/thread/d7cb8844-6774-4a98-8aa3-85e445af4867/

既然是By Design,就只有繞道而行。Survey了一下,找到一些建議做法:

  • 改用XML
  • 使用DataSet
  • 另外自訂Class作為參數
  • 以DictionaryEntry[]瓜代之

評估了一下,我原本想要借重的就是Dictionary Key/Value的單純資料結構,XML為開放格式不易限制成Key/Value的形式;小小需求動用到DataSet略嫌笨重;自訂Class在編譯時期就要確定Key的種類,不符本案例的前題。看來DictionaryEntry[]較合需求,因此我試寫如下: (剛好Dictionary與DirectionaryEntry的雙向轉換都有示範到)

[WebMethod]
public DictionaryEntry[] Test(System.Collections.DictionaryEntry[] entries)
{
    //用ListDictionary主要是為了稍後可以直接CopyTo轉DictionaryEntry[]
    //若有效率或其他考量,可改用其他Collection Class
    ListDictionary dct = new ListDictionary();
    foreach (DictionaryEntry de in entries)
        dct.Add(de.Key, de.Value);
    
    //Do something on the Dictionary
    //... blah blah blah ....
    if (dct.Contains("Kuso"))
        dct["Kuso"] = "殺很大";
    
    DictionaryEntry[] result = new DictionaryEntry[dct.Count];
    dct.CopyTo(result, 0);
    return result;
}

呼叫端範例如下:

protected void Page_Load(object sender, EventArgs e)
{
    localhost.AFAWebService aws = new localhost.AFAWebService();
    aws.Credentials = CredentialCache.DefaultCredentials;
    Dictionary<string, string> dct = new Dictionary<string, string>();
    dct.Add("Kuso", "你不要走");
    //DictionaryEntry在Web Service傳遞時會被當成自訂類別
    //因此要用namespace.DictionaryEntry而非System.Collections.DictionaryEntry
    List<localhost.DictionaryEntry> lst = new List<localhost.DictionaryEntry>();
    foreach (string key in dct.Keys)
    {
        localhost.DictionaryEntry de = new localhost.DictionaryEntry();
        de.Key = key;
        de.Value = dct[key];
        lst.Add(de);
    }
    localhost.DictionaryEntry[] result = aws.Test(lst.ToArray());
    Dictionary<string, string> dctRes = new Dictionary<string, string>();
    foreach (localhost.DictionaryEntry de in result)
        dctRes.Add(de.Key.ToString(), de.Value.ToString());
    Response.Write(dct["Kuso"] + "->" + dctRes["Kuso"]);
    Response.End();
}

經過這番來回折騰,這方法看來也不怎麼簡潔。

於是,我又嘗試了Paul Welter的SerializableDictionary物件,做法上要在Web Service與Client端都Reference這個自訂物件,而且使用Visual Studio的Add Web Reference時,自動產生的Proxy Class宣告中SerializableDictionary會被當成DataSet而失敗,因此得改成手動產生Proxy Class後再將DataSet改回SerializableDictionary:

C:\AppCodeFolder\>wsdl http: //localhost/myweb/afawebservice.asmx?WSDL /l:cs /n:localhost /out:AfaWebServiceProxy.cs
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 2.0.50727.42]
Copyright (C) Microsoft Corporation. All rights reserved.
Writing file 'AfaWebServiceProxy.cs'.

 

用了SerializableDictionary後,程式碼簡化許多:

[WebMethod]
public SerializableDictionary<string, string> Test(
 SerializableDictionary<string, string> dct)
{
    if (dct.ContainsKey("Kuso"))
        dct["Kuso"] = "殺很大";
    return dct;
}

呼叫端也很單純:

protected void Page_Load(object sender, EventArgs e)
{
    localhost.AFAWebService aws = new localhost.AFAWebService();
    aws.Credentials = CredentialCache.DefaultCredentials;
    SerializableDictionary<string, string> dct = 
             new SerializableDictionary<string, string>();
    dct.Add("Kuso", "你不要走");
    SerializableDictionary<string, string> dctRes = aws.Test(dct);
    Response.Write(dct["Kuso"] + "->" + dctRes["Kuso"]);
    Response.End();
}

但是,這個做法需要在Web Service與Client端加入自訂元件參照、Proxy Class需要手動增加或修改,還是有些許不便。這樣看來,DataSet或XML法雖有其他缺點,但內建支援的特點,在力求簡單的場合裡,倒也值得納入考量吧!

【延伸閱讀】


Comments

# by 大估

黑暗大…想不到你也被瑤瑤洗腦了~~

# by Wizard

台灣史上第一個,女主持人曾經 轉過身用背部主持的節目(肚兜裝),超誇張。

# by alex

I implemented a WCF service with Dictionary<string, string> as return type. Maybe it'd be a bit easier. But it requires .NET 3.5.

# by Jeffrey

to 大估/Wizard,說實在話,我是Google這支鬼廣告後才知到有"瑤瑤"這號人物哩。(電玩節目不是我的菜... XD) 這支CF跟光良的"想吃我?"很有得拼,經我審慎評估之後,二者無分軒輊,不相上下。 to alex, 我也蠻想改用WCF,不過因為是在一個團隊合作專案裡,會涉及後續其他人的修改維護,最後還是選擇用Web Service以免節外生枝。

# by 大估

還有一個廣告…全聯的 很多、很多、很多...

# by WizardWu

本也想提 wcf 的,wcf 連 object type 也能傳, 沒試過 Collection,原來 wcf 也能傳 Collection class。 看來 wcf 的學習投資報酬率還要等幾年,頂多是面試 換工作時可以用來唬蘭用的。

# by Max

我自已建立一個 WebService ,Method 的 signature 跟你一樣, aspx 的 button click 去建立 WebService 接著call Process , 沒有你說的那個例外訊息呢?是在什麼情況下會發生的呀??

# by Jeffrey

to Max, 剛才試了一下,用VS2008在Web Site Project裡新增一個WebService.asmx,加入 [WebMethod] public Dictionary<string, string> Process(Dictionary<string, string> dct) { return dct; } 接著View In Browser就會出現錯誤,你試看看結果是否跟我相同?

# by Max

to 黑暗大 我照你說的方式,還是並沒有出現這個錯誤也。謎~ 我這裡是 vs 2008 sp1 , .net 3.5 sp1

# by Ark

補充 W7 7100 64bit & server 2003 會閃這樣訊息(目前) 映像裡XP 好像會過(去年) 但是不必當他是bug,可以把他當作~不給別人看的webservice 這是在XML序列化的部分出錯~Web Reference WSDL 對不出XML要填的node ...但是某方面是可以用的~用js+json直接去填 WebMethod 會對Request.InputStream 反序列回來,近到程序,再正常的return(但是MVC Controllers不吃,1.0板處在無視的狀態, 和PageMethods也不搭,routemap也XD......算了離題不提了) 也就是3.5大推的刊在內頁aspx +PageMethods的方式~強調的是...短小精幹~直接server 與client 交易 XML 肥雖肥~但是一些server 與server的交易還是靠他的肥

# by 小熊子

謝謝,我用 implements IDictionary serialize 這些關鍵字找不到,後來直接改用 WCF 來做,直接跳過 Web Service.....

# by 小熊子

To be XML serializable, types which inherit from IEnumerable must have an implementation of Add(System.Object) at all levels of their inheritance hierarchy. System.Collections.Specialized.StringDictionary does not implement Add(System.Object). 我用了 System.Collections.Specialized.StringDictionary 也是有類似的困擾,解決的方式一樣,改用 WCF 就好了

Post a comment