NameValueCollection類別快速轉QueryString()之作法

今天剛好要用System.Net.HttpWebRequest撰寫一個Form Post的動作,裡面需要組合一些欄位,因此遇到這個以前都沒有發現過的事情,特此紀載。這故事很長,長話短說就是,如果你今天是在.NET Framework 4.5.2以下的平台且有UrlEncodeUnicode()編碼的顧慮,請「自幹」。如果已經在.NET Framework 4.5.2含以上,請用ParseQueryString()會快很多。

NameValueCollection直接ToString()並不如想像

如果你對NameValueCollection直接ToString(),會得到回應System.Collections.Specialized.NameValueCollection,而沒有像想像中的幫你組合好HTTP QueryString的組合(cKey1=OO&cKey2=XX...),這時候我們一定覺得這簡單,拿起LINQ馬上硬幹起來...

string cQueryString = String.Join("&", oNVC.AllKeys.Select(x => $"{x}={System.Web.HttpUtility.UrlEncode(oNVC.Get(x))}));

寫完後想想,且慢,微軟會設定這個NameValueCollection,在網路氾濫的現代,應該絕大部分都是用在QueryString居多吧,這樣說來應該有更快速產生QueryString格式的方法啊?於是去網路查了一下,發現有人分享這個小密技:

用System.Web.HttpUtility.ParseQueryString(String.Empty);實例化就好了
用System.Web.HttpUtility.ParseQueryString(String.Empty);實例化就好了
用System.Web.HttpUtility.ParseQueryString(String.Empty);實例化就好了

當下心裡想,三小!結果一試下去還真的可以直接ToString(),而且連Value都直接幫你System.Web.HttpUtility.UrlEncode()了,這簡直太神奇了。

System.Collections.Specialized.NameValueCollection oNVC = System.Web.HttpUtility.ParseQueryString(String.Empty);
oNVC.Add("cKey1", "Test");
oNVC.Add("cKey2", "<p>Html</p>");
oNVC.Add("cKey3", "中文字");
Console.WriteLine(oNVC.ToString());

輸出字串(可以看到中文或特殊符號都被編碼了):

cKey1=Test&cKey2=%3cp%3eHtml%3c%2fp%3e&cKey3=%e4%b8%ad%e6%96%87%e5%ad%97

原來神奇的寫法是把NameValueCollection當作Interface介面在使用

知道這種神奇的寫法與結果後,當下去翻MSDN簡直驚呆了。

1. HttpUtility.ParseQueryString真的會回傳NameValueCollection類別,https://docs.microsoft.com/zh-tw/dotnet/api/system.web.httputility.parsequerystring?view=net-5.0

2. NameValueCollection類別本身ToString()方法並沒有提供太特別的實作(所以才會拋出System.Collections.Specialized.NameValueCollection)。https://docs.microsoft.com/zh-tw/dotnet/api/system.collections.specialized.namevaluecollection?view=net-5.0

基於第1、2點,那是誰提供如此爽快的ToString()覆寫呢?這中間一定有問題,趕快翻.NET原始碼,找到這個:

3. HttpUtility.ParseQueryString原始碼(https://referencesource.microsoft.com/#System.Web/httpserverutility.cs,a711aeeae301c09c),可以看到其實是回傳HttpValueCollection。

public static NameValueCollection ParseQueryString(string query) {
  return ParseQueryString(query, Encoding.UTF8);
}

public static NameValueCollection ParseQueryString(string query, Encoding encoding) {
  //...略...
  return new HttpValueCollection(query, false, true, encoding);
}

看到這裡都傻了,HttpValueCollection是三小,從來沒有在System.Web下看過啊?這又跟NameValueCollection有何關係?再追下去:

4. HttpValueCollection原始碼(https://referencesource.microsoft.com/#System.Web/HttpValueCollection.cs,fde6b9ec5f1ed58a

[Serializable()]
internal class HttpValueCollection : NameValueCollection {
  //看到這裡才知道原來是internal Class,且此類別繼承自NameValueCollection
}

5. 快來看一下是否有覆寫ToString(),果然有。(https://referencesource.microsoft.com/#System.Web/HttpValueCollection.cs,290

6. 再來看一下是否有偷偷幫我們UrlEncode,果然有。https://referencesource.microsoft.com/#System.Web/HttpValueCollection.cs,352

這個UrlEncodeForToString裡面有一個關鍵資訊需要特別注意,就是如果有被設定DontUsePercentUUrlEncoding = true,就會採用後期統一的System.Web.HttpUtility.UrlEncode(),如果DontUsePercentUUrlEncoding被設定false,那就會使用不再被推薦使用(非標準)的System.Web.HttpUtility.UrlEncodeUnicode()。這只好再往下AppSettings.DontUsePercentUUrlEncoding。

7. 追到這邊才算是透徹的了解DontUsePercentUUrlEncoding設定(https://referencesource.microsoft.com/#System.Web/Util/AppSettings.cs,413),在這個屬性已經很清楚的說明差異之處了,只有.NET Framework大於等於4.5.2版本,才會被預設設為true。

false - use UrlEncodeUnicode for some URL generation within ASP.NET, which can produce non-compliant results
true - use UTF8 encoding for things like <form action>, which works with modern browsers
defaults to true when targeting >= 4.5.2, otherwise false

這個DontUsePercentUUrlEncoding沒有辦法動態指派,一般來說我們不太可能會跑去web.config再單獨設定這個參數,所以建議.NET Framework 4.5.2以下的人,還是自幹ToQueryString()比較快,反正也不用幾行就寫完了。

※補充一:無論是UrlEncode()或是UrlEncodeUnicode(),透過Form Post過去到遠端的ASP.NET一樣會觸發ASP.NET框架判定為傳遞違法內容問題,出現「具有潛在危險 Request.Form 的值已從用戶端...」等字眼,這種情況可使用雙重編碼繞過UrlEncode(UrlEncode(""))。

※補充二:若要在web.config中設定DontUsePercentUUrlEncoding的值,請參考下方XML:

<?xml version="1.0" encoding="utf-8" ?>
<appSettings>
  <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />
</appSettings>
HttpWebRequest HTTP Form Post Data Column NameValueCollection Encode UrlEncode QueryString