Quantcast
Channel: 黑暗執行緒
Viewing all 2311 articles
Browse latest View live

Hash對講機-IFrame跨站台網頁通訊程式庫

$
0
0

這是最近跟老IE纏鬥衍生的副產品。

雖然已研究出用document.domain克服無法與IFrame跨站台網頁溝通問題,但實務上常不免會遇到使用IP、機器名稱或別名連上網站的場合,document.domain法只適用URL採FQDN完整網域名稱且後段網域相同的情境,實用性大減。

針對IFrame跨站台溝通,浏览器同源政策及其规避方法一文提到三種解法:

  • window.name
  • 跨文件傳輸API(Cross-Document Messaging)
  • 片段識別碼(Fragment Identifier,指xxx.aspx#yyy,#號之後的部分,可透過location.hash取回)

Cross-Document Messaging API是HTML5時代產物,IE5/IE7相容模式玩不起,直接跳過。window.name沒有資料長度限制,但IFrame每次載入網頁只能傳一筆資料(參考),無法持續溝通,用法受限。location.hash做法利用父網頁與IFrame可以設定對方location(只設定不讀取即不違背同源政策)且xxx.aspx#aaa改為xxx.aspx#bbb不會重新載入網頁的特性,巧妙實現跨站台父網頁與IFrame雙向溝通。大部分瀏覽器有onhashchange事件即時監控location.hash變化,IE8以下則要自己寫setInterval監測,倒也不難解,如此就算通吃所有IE模式。上吧,location.hash,就決定是你了。

寫完雛型實測可行後,為方便應用我抽取成程式庫,命名為HashTalkie(Hash對講機),感覺還有未涵蓋情境跟改善空間,索性把程式碼放上Github,分享之餘也蒐集回饋,有利於持續改版強化。

https://github.com/darkthread/HashTalkie

程式碼及說明已放在Github,有興趣的朋友請自取。

另外我也弄了Live Demo,如以下示範,htmlpreview.github.io的parent.html內嵌來自rawgit.com的child.html,跨站台傳送蘭亭集序跟關公千里走單騎呢,我沒唬爛吧!XD


沒下載東西只是看網頁也會中毒?快檢查Flash Player版本

$
0
0

Flash漏洞儼然己成為病毒、木馬入侵的一條捷徑,前陣子鬧得沸沸揚揚的勒索病毒大爆發,經調查就是經由雅虎網頁廣告傳播,透過Flash漏洞感染,再加上使用者未開啟UAC,只是瀏覽網頁就中毒。

因為Flash沒更新,不過上網看個網頁就中獎,怎麼想都覺得可怕。更何況不是去什麼見不得人的網站充實D槽,也沒有胡亂下載安裝軟體,只不過看了每天都可能光臨的入口網站就中鏢,像是走在路上無端被招牌砸頭,無比冤枉。因此,得保持Flash即時更新到最新版,才能讓自己消災解厄!

研究之後,我找到一個檢查Flash版本的好方法,連上Adobe Flash Player的關於頁面-http://www.adobe.com/tw/software/flash/about/,網頁會同時顯示目前安裝Flash版本以及各平台最新版本清單,比對下版本數字,即可確保電腦已安裝最新版Flash,將中毒風險降到最低。

連上網頁後,如下圖所示,網頁下方有個表格列出各平台各瀏覽器的最新版本,而紅框內則是瀏覽器目前安裝版本,二者版號一致代表已安裝最新版。

另外,Flash有自動更新功能,建議開啟。檢查自動更新設定可在上圖紅框位置按右鍵選「全域設定」:

請確認「更新」頁籤的更新設定為「允許Adobe安裝更新」或「通知我安裝更新」,也可降低Flash版本太舊中毒的風險:

另外,Windows 7(Vista)起有個「使用者帳戶控制設定」(UAC)功能,遇到有程式在背後變更Windows設定時會跳出提醒。如果沒有安裝程式卻冒出提醒,你就該提高驚覺,判斷變更設定的程式來源是否可靠再決定要不要放行。遇上惡意程式攻擊時,有時能救你一命。參考:多想兩分鐘,你可以不必教User關掉Vista UAC

UAC設定共有四級,建議大家維持預設值第二選項(第一選項更安全,但連手動改設定都會跳警告有點煩人,只適合安全偏執狂),不要切成第三及第四選項。

關於UAC的更多細節,可參考:

坊間有一大堆教你把UAC關掉的教學,如同拆掉警報器換取耳根清靜,但相對在危險關鍵時刻也少了一層保命防護,大家自行斟酌吧!

2016-06-19 補充:與Demo聊到,在Google上查UAC教你關掉它的文章比介紹其用途的文章多一倍,原因之一應是當年Vista時代剛推出時設計不良太過煩人。但歷經改良後,現在的UAC己經比當年機靈通人性,不再那麼惹人生厭。如果你因為當年的壞印象或聽了別人的說法才關閉它,基於UAC在本事件護駕有功,建議開啟它再體驗一次是否能接受,畢竟,良藥苦口。 如果試用後還是決定關閉,願原力與你同在!:P

【獅子鬃毛】不用IE就不會中加密勒索病毒?

$
0
0

前陣子媒體流傳一種說法,「專家」依據PTT爆出大串中勒索病毒的求救文,爬文後歸納理出心得:不要用IE(傳說中的「這瀏覽器」)、不要上中國網站!

真相未明前,靠著歸納線索推測嫌犯無可厚非,但在欠缺科學方法驗證前驟下結論,很容易冤枉無辜者。單依統計做結論,可信度更是堪慮,例如:柯南是死神無疑、醫院是最危險的地方,不然為什麼那麼多人死在醫院…

一時之間,IE吞下所有罵名,成為大家口中該死的老賊、資安界的豬隊友。終於,在幾天後有資安專家揭開本次風波的真相

兇手不是IE,而是沒更新的舊版Flash;而散播者不是中國網站,而是台灣雅虎網頁(tw.yahoo.com)!

依據網路攻防戰的解析

這波惡意攻擊利用入口網站廣告散播有毒Flash,利用Flash 21.0.0.213之前版本的安全漏洞執行惡意程式,加上使用者的Win7沒啟用UAC(所以,多想兩分鐘,你可以不要關掉UAC)未能擋下攻擊,轟!推測有毒廣告是在五月底或六月初上架,使用舊版Flash的電腦光瀏覽網頁就會中獎,於是在六月初形成一波熱潮(6/3左右)。6/9雅虎接獲通知將廣告下架,問題網域也被下線,中毒案例迅速減少。(Windows 8的Flash已交由Windows Update自動更新,只要定期更新,也能降低Flash漏洞受攻擊的風險。)

由此可知,問題關鍵在沒更新的Flash,舊版Flash有漏洞,就算用Chrome、Firefox也可能中獎,IE非絕對關鍵。而病毒會觸發UAC警告,若使用者沒關掉UAC且意識到問題拒絕執行可疑動作,也有機會躲過一刧。

2016-06-19 補充-有朋友反應,Chrome/Firefox有所謂的Flash Sandbox,而IE也有Protected Mode,Flash Player會善用瀏覽器這些安全特性降低風險,至於在本案例中這些措施是否能防堵攻擊,我尚未找到明確資料。另外,Chome有個功能挺好,會封鎖不安全的舊版Flash Player,必須承認就這點讓Chrome更安全。

可惜的是,真相大白之後,「專家」跟媒體並沒有再次跳出來提醒大家,上回說不要用IE的講法不夠精確,隨時更新Flash跟不要關閉UAC才能保安康… 普羅大眾腦海留下的只有「IE很爛,超級不安全」「不要用IE」,沒更新的Flash還是沒更新,等待下回再與惡意Flash相會,繼續吞下各式各樣的病毒木馬,爆出絢爛火花。

如果讀者有幸看到這篇文章,請:

最後,不少當初對IE補刀的朋友好像該還IE一個公道,說聲:「對不起,IE,我錯怪你了」~

不過,反正都被罵這麼多年了(寫網頁的應該三不五時都碎唸過,我也不例外),也不差這一次了… XD


IE表示:何汝大當時的心情我能懂!

【茶包射手日記】Visual Studio手動加入Config檔無效

$
0
0

同事報案,用Visual Studio跑自動測試發現NLog沒作用。

前陣子整理過NLog問題偵錯技巧,熟門熟路啟動SOP:

  • 先在NLog.config加入<nlog throwExceptions="true>,未發現執行錯誤
  • 使用NLog.LogManager.Configuration.FindTargetByName("f")測試得到null,比對其他可正常運作NLog.config,確認<target xsi:type="File" name="f" …>寫法無誤

詢問案發經過,得知問題NLog.config今天才被手動加入專案。靈機一動,檢查NLog.config的項目屬性,犯人現形:

「Copy to Output Directory」(複製到輸出目錄)被設定成Do not copy,編譯時不會複製到bin目錄,故測試程式一直處於沒有NLog.config的狀態,就能合理解釋FindTargetByName("f")為何傳回null。

經過實驗,透過Add /New Item或Existing Item新増.config檔,Copy to Output Directory屬性預設值都是Do not copy。此一行為不致影響Web專案測試,但一遇上Console、Windows Form或測試專案,.config沒有複製到exe或dll目錄,設定無效。

將Do not copy改為Copy if newer重跑測試,NLog運作即告正常。

不過留下一個疑問,為什麼透過NuGet安裝的NLog.config就沒這問題?答案在 \packages\NLog.Config.4.3.3\tools\Install.ps1 裡:

param($installPath, $toolsPath, $package, $project)
 
$configItem = $project.ProjectItems.Item("NLog.config")
 
# set 'Copy To Output Directory' to 'Copy if newer'
$copyToOutput = $configItem.Properties.Item("CopyToOutputDirectory")
$copyToOutput.Value = 1
 
# set 'Build Action' to 'Content'
$buildAction = $configItem.Properties.Item("BuildAction")
$buildAction.Value = 2

全案宣告偵破,收隊!

取消安裝Application Insights

$
0
0

Application Insights是一套雲端服務監控系統,透過簡單設定就能蒐集ASP.NET網站伺服器及使用者資訊,例如:使用者使用的瀏覽器種類、到訪頁面(類似Google分析的功能),另外也能蒐集伺服器反應時間、請求統計與錯誤訊息等,還能設定檢查,偵測網站是否活著。(延伸閱讀:Sky的文章-Azure - Application Insights官方介紹

Visual Studio不知從哪個版本起,建立ASP.NET專案時會自動幫你安裝Application Insights,有時我只想弄個小網站測個功能,赫然發現專案裡的Application Insights模組向我揮手:「我準備好了,讓我們上雲端吧!」(啊,拎杯是唯一的User,等下測完就要把你清掉耶)。比較困擾的是新開公司內部網站專案,寫了一陣子才發現專案裡多了這些,從NuGet移除還挺費工夫:

有了幾次經驗學到一招,請如下圖取消右側的勾選(取消時記得說「謝謝,我不需要」才禮貌),就能省去事後移除的麻煩囉!

【茶包射手日記】NuGet Packager編譯卡住無回應

$
0
0

自從安裝Visual Studio NuGet Packager套件後,製作及上傳NuGet Package的工作輕鬆許多。隨著應用範圍擴大,發現一個問題:當打包項目變多,有時會發生編譯後一直停在編譯狀態永遠不結束,只能強制中止。試著增減打包項目,反覆測試後確定一點,當住與項目數目無絕對關聯,而是「項目愈多,出問題機率愈高」。

為追查問題看了NuGetPackage.ps1,才對NuGet Packager運作原理有點了解。原來它在背後是透過PowerShell Script使用程序物件啟動nuget.exe,呼叫WaitForExit()等待程序結束,透過StandardOutput、StandardError擷取輸出,再依ExitCode判斷作業成功或失敗。

測試直接執行「nuget pack Package.nuspec –Verbosity Detailed」,無論項目多寡都能順利完成。我這才注意到,原先編譯無反應時下,其實打包檔已順利產生。換句話說,若要精準描述問題,應為「當打包項目較多,有時會出現NuGet跑完但WaitForExit()沒被觸發,造成使用者空等」,試著寫成WaitForExit(10*1000)加上10秒逾時設定,編譯狀態便會在10秒後結束且結果正確,驗證問題出在WaitForExit()未正確偵測到程序結束。

依此推論是PowerShell WaitForExit的Bug,朝此方向爬文,在官網QA找到相同問題回報,而有網友提供一則決方案,將$packageTask.WaitForExit()如下圖移至$output及$error字串讀取動作的後方,果真解決了問題。

以上經驗提供使用NuGet Packager的朋友參考。而本次調查另一項收獲是見識到NuGet Packager利用PowerShell存取專案項目進行作業,這個技巧對開發自動化很有用處,收入錦囊~

網頁內嵌JSON建立JS物件之日期轉換問題

$
0
0

題目讀來有點不知所云,用實例講解才會清楚。在ASP.NET MVC Controller端建立的物件,想在輸出View的同時轉成JavaScript端物件,最直覺的做法是將物件轉為JSON字串,再以Razor語法內嵌一段var dataItem = { "num_prop": 1234, "str_prop": "ABCD", "bool_prop": true }; JavaScript語法,直接建立JavaScript物件。

但這個做法遇上DateTime屬性會有個小問題,例如:

public ActionResult Edit() 
{ 
    var item = new ColorData() 
    { 
        Index = 255, 
        Name = "Dark", 
        ConvTime = DateTime.Now, 
        Rank = 1, 
        RGBCode = "#000000", 
        Remark = "Very very dark..."
    }; 
    ViewBag.DataItem = JsonConvert.SerializeObject(item); 
return View(); 
}

View Edit.cshtml寫成:

<script>
var DataItem = @Html.Raw(ViewBag.DataItem); 
</script>

得到結果為:

var DataItem = {"Index":255,"Name":"Dark","RGBCode":"#000000",
"ConvTime":"2016-06-28T21:09:15.2008013+08:00","Rank":1,
"Remark":"Very very dark..."}; 

JavaScript的ConvTime屬性型別為字串,而我們期望的是Date型別。JavaScript端要將ISO 8601格式字串轉Date型別,最常見做法是在JSON.parse()時傳入DateReviver函式。依此概念衍生出兩種解法:

  • 將DataItem以JSON.stringify先轉成JSON字串,再用JSON.parse()+DateReviver轉回物件
  • 改寫為ViewBag.DataItemJson = JsonConvert.SerializeObject(JsonConvert.SerializeObject(item))跟
    var DataItemJson = @Html.Raw(ViewBag.DataItemJson);
    改傳字串到View端後再用JSON.parse()+DateReviver轉為物件

方法雖然可行,為了解決日期問題,JSON轉來轉去,怎麼看都有點蠢。心想,如果產生JSON時,用new Date(nnnn)取代"yyyy-MM-ddTHH:mm:ssZ",不靠游擊手轉傳,表演從全壘打牆邊直傳本壘的雷射肩,這才叫帥氣。

查了資料,Json.NET不虧是天神級的JSON兵器,有個JavaScriptDateTimeConverter可以協助我們實現願望。在SerializeObject時傳入new JavaScriptDateTimeConverter()當參數: 

public ActionResult Edit() 
{ 
    var item = new ColorData() 
    { 
        Index = 255, 
        Name = "Dark", 
        ConvTime = DateTime.Now, 
        Rank = 1, 
        RGBCode = "#000000", 
        Remark = "Very very dark..."
    }; 
    ViewBag.DataItem = JsonConvert.SerializeObject(item, 
new JavaScriptDateTimeConverter()); 
return View(); 
}


JSON字串中的ConvTime屬性值由ISO 8601格式字串會被改成new Date(nnnn),拿來宣告JS物件就直接是Date型別囉!

又到了呼口號時間:

Json.NET好威呀!

跨平台一大步,.NET Core 1.0正式登場!

$
0
0

這幾天在我FB洗版的大消息,莫過於.NET Core 1.0跟ASP.NET Core 1.0(原先命名為ASP.NET 5)已正式發佈!

Scott Hanselman,他加入微軟多年一直致力.NET與Open Source推廣,在15年後.NET Core 1.0推出的這一刻,終於攀上巔峰。

.NET Core讓C#走出Windows,正式登陸Mac、RedHat Enterprise Linux、Ubuntu Linux,支援C#、VB、F#,而整個.NET Core都Open Source並放在Github,開發人員可以取得原始碼,回饋問題,甚至找到Bug自己改,發現不足自已加,還能貢獻自己的修改結果,如果被.NET Core小組接受,就能跟別人說:.NET Core裡面有兩行是我寫的!(喂)

由Scott的文章,我整理出以下重點。

.NET Core具備以下特性:

  • 跨平台
    可在Windows、Mac、Linux執行(既然已Open Source,將來有機會靠社群之力拓展到更多平台)
  • 彈性部署
    可以跟程式一起部署,也可以每個使用者個別安裝或安裝於主機供所有使用者使用
  • 命令列工具形式
    .NET Core的所有相關程式都透過命令列方式執行
  • 相容性
    透過.NET標準程式庫與.NET Framework、Xamarin和Mono相容
  • 開放原始碼
    採MIT及Apache 2授權,文件採CC-BY授權,由.NET基金會管理
  • 微軟支援
    雖然開源,.NET仍是微軟的產品,享有產品支援

.NET Core包含以下部分:

  • .NET Runtime
    CoreCLR,負責型別系統、組件載入、記憶體回收(GC)、Interop(與Unmanaged程式溝通)及其他基本服務
  • Framework程式庫
    CoreFx,包含System.Collections, System.IO, System.Xml… 這些基本程式度
  • SDK工具編譯器
    CLI Tools與Roslyn編譯引擎,可以透過.NET Core SDK取得。
  • dotnet App Host
    用來選取並執行Runtime、提供組件載入原則並啟動.NET Core應用程式。SDK工具也是使用相同方式啟動。

如果你想嚐試.NET Core,最方便的方法是更新到Visual Studio 2015 Update 3再安裝.NET Core Tools for Visual Studio。(如果你還沒裝VS2015,可以考慮VS2015社群版,免費)

若覺得Visual Studio 2015太笨重,Visual Studio Code安裝C#擴充套件也是另一種選擇。至於Mac/Linux平台,就得靠命令列工具打通關。

.NET Core的文件在:https://docs.microsoft.com/dotnet,另外.NET Core官網:https://www.microsoft.com/net有個好玩的線上C#編譯介面,類似TypeScript Playgournd,可以寫一小段程式在雲端直接執行:

另外還有一個C#教學網站,教學內容還針對JavaScript、Java、VB6、C++背景的開發人員設計,很有誠意。

盼了十幾年,終於等到這一天,未來要在Linux平台寫程式,總算有火力強大的制式武器可用了!萬歲~


Coding4Fun-也來IoT好了,Raspberry Pi 冰箱散熱溫度監測系統

$
0
0

故事要從家裡服役十七年的老冰箱掛點講起,老冰箱這兩年百病纏身,冷度不足,門框磁膠條密合不佳,冰箱兩側散熱區溫度偏高… 加上老機型耗電,早有換新念頭,逛賣場也常在冰箱區留連,但換冰箱茲事體大令人不想面對,總缺少臨門一腳。上週起冰箱兩側忽然熱到燙手,然後,它就死掉了... 不得不啟動應變計劃,尋找食材暫放空間,評估冰箱廠牌款式,比價找供應商,忙得不亦樂乎。冰箱暴斃事件,讓我再次對「重要但不緊急的事,拖久就會變重要又緊急」有深刻體認!共勉之。

感念老冰箱苦撐十七年護國有功,挑了同廠牌新一代六門有製冰機外加能源效率一級的新機型。新冰箱就位後有個小問題-新機容量大,機身較寬( 72cm –> 77cm),離牆面距離變短,現代冰箱靠左右兩側跟上方散熱,依說明書要求兩側離牆要2cm以上,實測右側是有2 - 3.5cm的空隙。作業系統說記憶體至少2G,真的只裝2G RAM,效能通常只有「堪用」的水準,這個最小離牆距離夠不夠?會不會導致兩側溫度偏高增加耗電量?吾人以為,只有實測才能解答。

於是我啟動了「外掛式物聯網強化計劃」,構想是用Arduino、Espruino或Raspberry Pi連接溫濕度感應器(Sensor),定期測量溫度上傳到網站,再整理成圖表。要上傳資料,支援USB無線網卡的Raspberry Pi是最簡便的選擇,從書架上積滿灰塵的盒子拿出買來只玩幾天就打入冷宮的Raspberry Pi [我的Arduino、Espruino好像都是同樣下場 (笑) ],重見天日的 Pi 說:皇上,臣妾想你想得好苦哇~

手邊有的溫濕度感應器是DHT11,原本想循往例依照DHT11的通信規格用Mono C#自幹GPIO驅動物件,但DHT11資料傳輸時序精準度要到Microsecond(百萬分之一秒)等級,C#的Thread.Sleep只有Millisecond(千分之一秒)玩不起。重新評估後決定改走Python(另一個選擇是C/C++,跟魔戒或薩魯曼同隊心理壓力超大,我還是跟樹精做朋友就好),程式語言門檻較低,現成程式庫或範例也多。

先完成硬體接線,DHT11只有三條線,3.3V或5V電源、接地與訊號,訊號線我選擇接在GPIO4(圖中白線)。

軟體部分我先試了網路上很多人提到的Adafruit_Python_DHT,但卡死在用sudo執行還是噴出Error accessing GPIO錯誤,網路上相關討論眾多無明確結論,只知似與Raspberry Pi OS版本有關。另外找到透過pigpio Daemon的做法,單一.py程式搞定感覺較單純易偵錯。前題要先執行pigpio Daemon,pigpiod會以root身分執行,另開Socket介面讓程式以一般身分操作GPIO。我試出來的安裝步驟是:

wget abyz.co.uk/rpi/pigpio/pigpio.zip
unzip pigpio.zip
cd PIGPIO
make
make install

接著sudo pigpiod,dht11.py裡sensor = DHT11(pi, 4)也是走GPIO4針腳跟我的實體接法相同,不用改就能跑,得到溫度與濕度數字代表執行成功:

下一步是建置資料蒐集機制,雖然寫個幾行ASP.NET網頁就能搞定,但不想為了喝牛奶養一頭牛,我找到有圖表功能又容易上手的雲端資料蒐集服務-http://ubidots.com,免註冊用Facebook、Google、Twitter或Github帳號就能登入取得API Key,API教學非常詳細易懂,5個以下Sensor變數不收費,又有線上圖表可看,超級方便。

搞懂API呼叫方法後,在dht11.py中加入程式碼將溫度上傳到ubidots API,SSH登入Pi用nano改程式有點蹩腳,平日習慣Visual Studio豪華的開發環境,這回像是用慣衝鋒槍的突擊隊員被要求拿小彈弓上戰場,充滿無力感。想起Visual Studio 2015支援Python編輯,原本只想借重它的語法高亮顯示跟大視窗,卻驚喜地發現Python Tools for Visual Studio (PTVS ) 支援Goto Definition、Find All References、變數更名、Intellisense 顯示註解,還能設中斷點。順手查了文章,更發現它甚至能遠端連線 Raspberry Pi 進行偵錯,即時顯示區域變數值… 嚇得我滿地撿下巴。(PTVS遠端偵錯示範影片

Visual Studio 住海邊無誤!

(深夜在粉絲專頁貼完感想拾獲一篇MSDN中文介紹文,有興趣的朋友可以一讀:VS 上開發 Python 你不可不知道的六大功能

在Python要發送HttpRequest,Requests程式庫是首選,用起來跟C#的WebClient一樣方便,幾行就能搞定跟WebServer往來的大小事。修改dht11.py,一開始import requests,再加入幾行:

if __name__ == '__main__':
    pi = pigpio.pi()
    sensor = DHT11(pi, 4)
    tempVarId = "5776...略...a49b"
    humdVarId = "5776...略...bbd4"
    url = "http://things.ubidots.com/api/v1.6/variables/"
    pathAndToken = "/values?token=bQqr...略...nxnP"
    headers = { 'content-type': 'application/json' }
for d in sensor:
        print("temperature: {}".format(d['temperature']))
        print("humidity: {}".format(d['humidity']))
try:
            requests.post(url + tempVarId + pathAndToken, \
            data='{ "value": ' + str(d["temperature"]) + ' }', headers=headers)
            requests.post(url + humdVarId + pathAndToken, \
            data='{ "value": ' + str(d["humidity"]) + ' }', headers=headers)
        except:
            print("upload error")
        time.sleep(60)
    sensor.close()

最後還有一關,要能將程式丟到背景跑,才能在退出SSH終端後繼續執行,我選擇用nohup來做:

Raspberry Pi 用USB充電器供電放在冰箱上方,將DHT11垂吊在冰箱與牆壁之間,透過無線網卡登入終端以nohup啟動dht11.py,我的第一個IoT專案-「冰箱散熱器溫度監測系統」就正式上線囉~

附上今天初跑五小時的統計,溫度在30-38度間游離,沒有想像來得熱,我想2公分的間距足夠。至於會不會在某些條件下持續飆高,反正監測系統已成,就進行長期觀察囉~

在JavaScript模擬C# Dictionary、LINQ Where、Select與OrderBy

$
0
0

一週內被兩位同事問到幾乎相同的問題,這一定是天意!趕緊寫篇FAQ以免天公伯不開心~

【問題】

  • 用JavaScript要怎麼實現Dictionary<string, T>?
  • JavaScript有沒有類似LINQ Where()、Select()、OrderBy()的東西?

回答第一個問題,JavaScript物件本身就具備Dictionary<string, T>的特性,範例如下:

<!DOCTYPEhtml>
<html>
<head>
 
<metacharset="utf-8">
<metaname="viewport"content="width=device-width">
<title>JS Dictionary&lt;string, T&gt;</title>
</head>
<body>
 
<scriptsrc="https://code.jquery.com/jquery-1.12.4.js"></script>
<script>
function Player(Id, Name, RegDate, Score) {
this.Id = Id;
this.Name = Name;
this.RegDate = RegDate;
this.Score = Score;
    }
var p1 = new Player("P1", "Jeffrey", new Date(1900, 0, 1), 32767);
var p2 = new Player("P2", "Darkthread", new Date(2016, 6, 2), 65536);
//使用JavaScript物件模擬Dictionary<string, Player>
var dict = {};
//加入或指定key為P1的內容
    dict["P1"] = p1;
    dict["P2"] = p2;
//讀取key為P1的項目
    console.log("P1.Name = " + dict["P1"].Name);
//檢查key是否存在
if (dict["P1"]) console.log("P1存在");
if (!dict["Q1"]) console.log("Q1不存在"); 
 
//模擬Dictionary<string, T>.Keys
//IE7、8相容
var keys = [];
for (var key in dict) keys.push(key); 
    console.log(keys);
//IE9+及其他瀏覽器
    console.log(Object.keys(dict)); 
//模擬Dictionary<string, T>.Values
var values = $.map(keys, function(key) { return dict[key] });
    console.log("values[0].Name=" + values[0].Name);
    console.log("values[1].Name=" + values[1].Name);
//移除指定key值項目
    delete dict["P1"];
if (!dict["P1"]) console.log("P1已移除");
</script>
</body>
</html>

執行結果:Live Demo

"P1.Name = Jeffrey""P1存在""Q1不存在"
["P1", "P2"]
["P1", "P2"]"P1已移除""P1.Name = Jeffrey""P1存在""Q1不存在"
["P1", "P2"]
["P1", "P2"]"values[0].Name=Jeffrey""values[1].Name=Darkthread""P1已移除"

補充,在TypeScript如要宣告Dictionary<string, Player>強型別,寫法為var dict: { ["key": string]: Player } = {};

問題二,JavaScript能否做到LINQ Where() Select() OrderBy()的效果?

類似需求我慣用jQuery.grep()jQuery.map()搞定,排序則可用JavaScript Array本身的sort()方法,但sort()會改掉陣列本身的順序,若要比照OrderBy()的效果,得先用.slice(0)另建一個複本,示範如下:

<!DOCTYPEhtml>
<html>
<head>
 
<metacharset="utf-8">
<metaname="viewport"content="width=device-width">
<title>JS LINQ Where(), Select() and OrderBy()</title>
</head>
<body>
 
<scriptsrc="https://code.jquery.com/jquery-1.12.4.js"></script>
<script>
function Player(Id, Name, RegDate, Score) {
this.Id = Id;
this.Name = Name;
this.RegDate = RegDate;
this.Score = Score;
    }
var p1 = new Player("P1", "Jeffrey", new Date(1900, 0, 1), 32767);
var p2 = new Player("P2", "Darkthread", new Date(2016, 6, 2), 65536);
//用陣列模擬List<Player>
var list = [p1, p2];
//Where(o => o.Score > 255)
var res = $.grep(list, function(o) { return o.Score > 255 });
    console.log("Where(o => o.Score > 255).Count=" + res.length);
//Select(o => new { PlayerId = o.Id, PlayerName = o.Name })
var res = $.map(list, function(o) { return { PlayerId: o.Id, PlayerName: o.Name }; });
    console.log("Select(o => new { PlayerId = o.Id, PlayerName = o.Name }).Count=" + res.length);
    console.log("res[0].PlayerId=" + res[0].PlayerId);
    console.log("res[1].PlayerName=" + res[1].PlayerName);
//OrderBy(o => o.Name)
//OrderBy不更動List<T>順序,在JavaScript要用slice(0)先複製新陣列物件
//以免sort影響原陣列排序
var sorted = list.slice(0).sort(function(a, b) {
if (a.Name === b.Name) return 0;
return (a.Name < b.Name) ? -1 : 1;
    });
    console.log("sorted[0].Name=" + sorted[0].Name);
    console.log("sorted[1].Name=" + sorted[1].Name);
//OrderByDecending(o => o.Score)
    sorted = list.slice(0).sort(function(a, b) {
if (a.Score === b.Score) return 0;
return (a.Score > b.Score) ? -1 : 1;
    });
    console.log("sorted[0].Score=" + sorted[0].Score);
    console.log("sorted[1].Score=" + sorted[1].Score);    
</script>
</body>
</html>

執行結果:Live Demo

"Where(o => o.Score > 255).Count=2""Select(o => new { PlayerId = o.Id, PlayerName = o.Name }).Count=2""res[0].PlayerId=P1""res[1].PlayerName=Darkthread""sorted[0].Name=Darkthread""sorted[1].Name=Jeffrey""sorted[0].Score=65536""sorted[1].Score=32767"

以上範例使用jQuery.grep()模擬Where()、用jQuery.map()模擬Select(),而在ECMAScript 5規格,JavaScript Array已加入foreachfiltermap等方法,可以取代jQuery的.each()、.grep()及.map(),但存在IE7/IE8不支援的限制需要留意。ES5內建的filter()與map()用起來跟jQuery版差不多,換用工程不大,以下為改寫範例: Live Demo

//Where(o => o.Score > 255)
var res = list.filter(function(o) { return o.Score > 255 });
    console.log("Where(o => o.Score > 255).Count=" + res.length);
//Select(o => new { PlayerId = o.Id, PlayerName = o.Name })
var res = list.map(function(o) { return { PlayerId: o.Id, PlayerName: o.Name }; });
    console.log("Select(o => new { PlayerId = o.Id, PlayerName = o.Name }).Count=" + res.length);
    console.log("res[0].PlayerId=" + res[0].PlayerId);
    console.log("res[1].PlayerName=" + res[1].PlayerName);

最後,如果你覺得以上做法不夠原汁原味,還是想在JavaScript執行正統的LINQ方法,例如:

var queryResult = Enumerable.From(jsonArray)
    .Where(function (x) { return x.user.id < 200 })
    .OrderBy(function (x) { return x.user.screen_name })
    .Select(function (x) { return x.user.screen_name + ':' + x.text })
    .ToArray();

有個Open Source專案-linq.js可以實現以上夢想。不過,該專案已有段時間未更新,採用前也應該入考量。而依我個人看法,既然在寫前端,不妨改變思維,依循JavaScript風格解決才是王道。既然已有簡便做法能滿足需求,硬要復刻還原過往的習慣,並不利於執行效能及團隊協作,用JavaScript簡便搞定還是較好的選擇。

【茶包筆記】jQuery AJAX呼叫在IE有問題

$
0
0

同事報案,某網頁使用jQuery.ajax()發出四個OData查詢,在Chrome執行正常,在IE時兩個AJAX呼叫正常,有兩個查不到資料。使用F12觀察,發現有問題的AJAX呼叫URL參數包含中文但未使用encodeURIComponent()編碼,Chrome正確地自動做了轉換,IE也自動做了轉換,但轉換結果出現亂碼。

URL未用encodeURIComponent()編碼要承擔敗戰責任無庸置疑,但jQuery.ajax()在不同瀏覽器結果不同這點挺有趣,值得調查,弄了精簡範例驗證這點:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
</head>
<body>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.12.4.js">
</script>
<script>
var url = "/_api/Query('執行緒')/items?$filter=Category eq  '黑暗'";
        $.get(url);
</script>
</body>
</html>

如圖所示,Chrome自動將URL中文部分成功轉成UTF8編碼:

而IE也做了轉換,但轉出亂碼:

換了jQuery 2.2.4甚至3.0.0,測試結果都相同。追入jQuery原始碼,jQuery.ajax()未對URL加工,交給瀏覽器內建XMLHttpRequest物件全權處理,換言之,是IE跟Chrome的XMLHttpRequest行為差異所致。

改寫程式碼如下,直接使用XMLHttpRequest,結果與jQuery.get()完全相同,證實全案的關鍵在於IE與Chrome XHR元件之URL轉換行為不同。

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
</head>
<body>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.12.4.js">
</script>
<script>
var url = "/_api/Query('執行緒')/items?$filter=Category eq  '黑暗'";
var oReq = new XMLHttpRequest();
        oReq.open("get", url, true);
        oReq.send();
</script>
</body>
</html>

結論,URL有中文或特殊字元請自行encodeURIComponent(),不要依賴瀏覽器自動轉換,以免因XHR處理邏輯不同產生非預期結果。

【茶包射手日記】程式當掉時Oracle Transaction未自動Rollback

$
0
0

同事報案,稍早發生Oracle錯誤導致有一批排程作業失敗,很快找到錯誤,修正後重新執行排程卻出現更新資料庫發生Timeout,而Oracle錯誤後才新増的項目則可順利執行無誤。

由以上線索,推測最可能的原因是「出問題的資料被鎖定了」。檢查程式碼,啟動Transaction但未使用try…catch主動於出錯時Rollback。依據過去經驗,Oracle的Transaction在Client Process意外結束後不會自動Rollback,過程產生的資料鎖定也一直留在特定資料上,如此即能解釋為何出錯的一批資料重新執行會Timeout(苦等不到鎖定釋放)而事後新増的項目卻沒問題。經驗裡SQL Server在Client Process不正常結束時會自動Rollback Transaction,而Oracle未Commit的Transaction則會持殘留,需要人為介入處理。請DBA查詢,證實Oracle殘留未結束Session及Lock,砍掉Session後再重跑排程,Timeout問題消失。

找到Stackoverflow一則討論有相關說明:

未Commit或Rollback的Oracle Transaction會導致無法登出,因此當Client端當掉或異常中止,可能會留下Session及鎖定,需由DBA手動刪除。查詢Sesion及鎖定可使用以下SQL語法:

SELECT ses.sid, ses.serial#, ses.username, ses.program, ses.osuser, ses.machine
FROM v$session ses,
       dba_blockers blk
WHERE blk.holding_session = ses.sid

確認為異常Session後可使用以下指令刪除:

ALTER SYSTEM KILLSESSION'<<sid>>, <<serial#>>'

若想免除人為介入,Oracle有個Dead Connection Detection (DCD) 機制,定期偵測Client端是否還活著,主動砍掉Client掛點的無主Session。不過,最根本解決這個問題的做法是改良程式,使用 try … catch 區塊攔截錯誤,出錯時主動Rollback Transaction,比依賴第三方機制擦屁股更可靠更有效率。

【茶包速記】從排程程式呼叫Word發生錯誤

$
0
0

同事報案,有支背後操控Office Word處理文的主控台應用程式(Console Application)以排程(Scheduled Task)方式執行,移機後無法執行。觀察現象為程式出錯終止,其啟動的Word程序殘留,要重複執行則因前次啟動Word仍鎖定特定文件檔出錯,錯誤Log則發現"There is insufficient memory. Save the document now."訊息。

刪除殘留Word程式,不經排程改由手動執行程式,則一切正常。

整理蒐證重點:

  1. 手動執行正常,用排程執行才會出錯
  2. 同一程式原本在Windows 2003運作正常,移至Windows 2012後才發生問題

由這些線索,我馬上聯想到相似案例-呼叫Excel的程式無法以排程方式執行,嘗試手動建立"c:\windows\syswow64\config\systemprofile\desktop",瞬間藥到病除。由此獲得結論,Word與Excel要以排程方式執行,均需手動建立Desktop目錄,症狀雖然不同,相同藥方一帖見效,特筆記備忘。

垃圾郵件出新招?由Apple官方親自寄送的垃圾信

$
0
0

Gmail信箱收到一封怪信,內容如下:

信件來自id.apple.com,第一行明顯是垃圾郵件廣告或是釣魚詐騙,但後方緊接著標準Apple ID救援帳號驗證碼通知,其中Apple ID帳號頁面URL也是連到真的Apple ID網站無誤。經驗裡,Gmail的垃圾信檢核能力強大,鮮少有人破關,若這封信發信來源是偽造的卻闖關成功,其中使用技巧令人好奇。

檢查信件的SMTP Header,在其中看到Received: from nwk-txn-msbadger0702.apple.com (nwk-txn-msbadger0702.apple.com. [17.151.1.81])字樣,幾可確認信件的確來自Apple的郵件伺服器,依我的理解,此一軌跡變造難度頗高,尤其郵件收發端分別是Google、Apple,伺服器被呼嚨或動手腳的機率應不高。

Delivered-To: email_address@gmail.com
Received: by 10.202.67.194 with SMTP id q185csp240909oia;
        Thu, 7 Jul 2016 21:25:34 -0700 (PDT)
X-Received: by 10.98.201.210 with SMTP id l79mr6426805pfk.87.1467951934894;
        Thu, 07 Jul 2016 21:25:34 -0700 (PDT)
Return-Path: <Do_not_reply@id.apple.com>
Received: from nwk-txn-msbadger0702.apple.com (nwk-txn-msbadger0702.apple.com. [17.151.1.81])
        by mx.google.com with ESMTPS id 84si1805734pfs.131.2016.07.07.21.25.34
        for <email_address@gmail.com>
        (version=TLS1 cipher=AES128-SHA bits=128/128);
        Thu, 07 Jul 2016 21:25:34 -0700 (PDT)
Received-SPF: pass (google.com: domain of do_not_reply@id.apple.com designates 17.151.1.81 as permitted sender) client-ip=17.151.1.81;
Authentication-Results: mx.google.com;
       dkim=pass header.i=@id.apple.com;
       spf=pass (google.com: domain of do_not_reply@id.apple.com designates 17.151.1.81 as permitted sender) smtp.mailfrom=Do_not_reply@id.apple.com;
       dmarc=pass (p=REJECT dis=NONE) header.from=id.apple.com
DKIM-Signature: v=1; a=rsa-sha1; d=id.apple.com; s=id2048; c=relaxed/relaxed;
    q=dns/txt; i=@id.apple.com; t=1467951934;
    h=From:Subject:Date:To:MIME-Version:Content-Type;
    bh=5set+Ft2cOnQ+FGcv894n8DdbKw=;
    b=IpLQ6msyhZ0r+AQzg+BBkW3RuYFPq+ZbG+XGuuN/v19FONAUCVAJIVDVX9irSmA5
    fGbIY6iwrbVtZzFIDLpHN5OZ5IZxJ7cT9wiKHDwIrNguCjxPX1MiezjEnYfhiMIH
    zQlOEq9lF5sAMff8LWOHuQ==;
Date: Fri, 8 Jul 2016 04:25:34 +0000 (GMT)
From: Apple <appleid@id.apple.com>
REPLY-TO: appleid_cnzh@email.apple.com
To: email_address@gmail.com
Message-ID: <2047128433.80483757.1467951934576.JavaMail.email@email.apple.com>
Subject: =?gb2312?B?0enWpMT6tcS159fT08q8/rXY1rc=?=
MIME-Version: 1.0

由此推測,信件真是由Apple ID所寄沒錯,那第一行被人加料又是怎麼一回事?搞得我好亂。

滿頭霧水,把疑問丟上臉書想徵召朋友腦力激盪… 一分鐘後,我忽然看懂還「噗」一聲笑了出來。

答案就藏在信件第一行,「万部高清片……,您好:」

猜想這是垃圾信廠商想出的新招,直接用「万部高清片……」廣告詞當成使用者姓名註冊AppleID,再把垃圾信寄發對象的電子郵件設成該Apple ID的救援郵件,依Apple ID的驗證機制,系統會寄驗證碼到指定的救援郵件信箱。信件由Apple ID官方郵件伺服器寄出,順利通過Gmail等各大郵件系統的安全檢查順利送達,而信件一開頭稱呼收信人「万部高清片……,您好:」,廣告或詐騙內容就這麼送到了,一路使用Apple的主機、頻寬,郵件還因Apple官方身分加持以暢行無阻,大吃Apple豆腐。

沒想到除了SQL Injection、Cross-Site Scripting(XSS),居然還有UserName Injection,這也太有創意了~

依我的看法,這點可視為Apple ID註冊機制的瑕疵,可透過限制使用者姓名長度、過濾不合理文字(例如:URL),或限制救援郵件設定次數改善。而由此一案例,未來自己在設計系統時,除了XSS,也要考量UserName Injection的可能性。

【笨問題】JavaScript取字串split()結果最後一段

$
0
0

困擾我很久的一個問題:寫程式時常遇到用Split切字串再取最後一節的情境,例如:「DomainName\Account」取Account、「Oracle.ManagedDataAccess.Client.OracleConnection」取OracleConnection。

這類需求用C#寫,當然二話不說,Split()加LINQ .Last()一行搞定:

"Domain\\Account".Split('\\').Last()

但同樣一件事搬到JavaScript我就發傻了,只會中繼傳球,無法由外野直傳本壘:

var ary = "Domain\\Account".split('\\');
var result = ary[ary.length - 1];

這招從VB年代寫到今天,除了囉嗦一點,也沒什麼不對。但平日一行就搞定的事硬是多生一個變數寫成兩行,怎麼都覺笨拙。今天認真爬文才猛然驚醒,屁股加個pop()不就好了。

"Domain\\Account".split("\\").pop();

為笨了這麼久乾一杯…


NuGet Package部署測試小技巧-清除Cache

$
0
0

聲明,本文介紹的技巧主要針對使用NuGet Package Explorer或Visual Stuio NuGet Packager套件自製NuGet Package且上傳NuGet私服的場合,如果你只是純粹的NuGet Package使用者,記個書籤或留個印象就好,未來有需要再回來。

先說說我遭遇的困擾,先前曾提過重複發行NuGet Package時版號必須比現有Package版號高,不然會上傳失敗。基本上就讓版號1.0.0、1.0.1、1.0.2逐次遞增就能解決,不是什麼大問題。不過若安裝程序較複雜,常需反覆實驗多次,除了一直要改.nuspec版號,若Package間存在相依性(例如SomeMvcLibrary Packaget 1.0.0指定<dependency id="SomeCoreLibrary" version="1.0.0" />,參考)就更頭痛了。dependency所指定版號為Package之最低版號要求,當SomeCoreLibrary Package失敗重新發佈升到1.0.1,除非SomeMvcLibrary同步修改為<dependency id="SomeCoreLibrary" version="1.0.1" />,自己也重新發佈1.0.1,否則下回安裝SomeMvcLibrary時一併安裝仍是有錯的SomeCoreLibrary Package 1.0.0。

在經歷過A依賴B、B依賴C,C一直測不過,改了又改,改了再改的連鎖改版地獄後,我體悟到「刪除NuGet私服上的舊版,繼續使用同版號發行」才是不傷身體的開發方式。但這又遇到另一個問題,這種做法形同偷吃步,違背「版號相同內容就該相同」的常理,而NuGet內建Cache機制,遇到版號相同時會優先使用Cache裡的內容,於是常出現NuGet私服同版號Package已悄悄更新,使用Visual Studio卻一直安裝有錯舊版本的窘境。

NuGet.exe有個指令可以解決這個惱人問題,下載NuGet.exe(或使用NuGet安裝NuGet.CommandLine Package),執行「nuget locals packages-cache –clear」可清除Cache:(參考:指令說明

以上指令會一次清除所有Cache,導致其他正常Package的Cache失效。最後我試出來的絕招:開啟使用者資料夾下的.nuget\packages,直接刪除有錯待更新的Package目錄,這應該是最快狠準的做法了。

以上經驗提供NuGet Package打包同業參考。

筆記:int?(Nullable)運算與??運算子優先順序

$
0
0

同事回報某段C#程式發現Bug:

int lastQty = 100; 
int? soldQty = null; 
int leaveQty = lastQty - soldQty ?? 0;

soldQty 由其他系統傳入可能為 null,原本我的想法是遇到 soldQty==null 就視為0,此時 leaveQty 應等於 lastQty,但以上程式執行結果與預期不同,leaveQty == 0!

這枚Bug隱藏了兩項疑問:

  1. ?? 運算子(Operator)與加減乘除誰先誰後?
  2. int與null加減乘除時結果會是什麼?

同事與我都不知道答案,爬文後才把這段空白知識補齊。

運算子優先順序大全

找到一篇超完整的C#運算子列表,運算子優先順序依序為:

  1. 主要運算子(Primary Operators)
    x.y, x?.y, f(x), a[x], a?[x], x++, x—, new, typeof, checked, unchecked, default(T), delegate, sizeof, –>
  2. 一元運算子(Unary Operators)
    +x, -x, !x, ~x, ++x, -x, (T)x, await, &x, *x
  3. 乘法類運算子(Multiplicative Operators)
    x*y, x/y, x%y
  4. 加法類運算子(Additive Operators)
    x+y, x-y
  5. 移位運算子(Shift Operators)
    x<<y, x>>y
  6. 關係和類型測試運算子(Relational and Type-testing Operators)
    x<y, x<y, x<=y, x>=y, is, as
  7. 等號比較運算子(Equality Operators)
    x==y, x!=y
  8. 邏輯AND運算子(Logical AND Operator)
    x&y
  9. 邏輯XOR運算子(Logical XOR Operator)
    x^y
  10. 邏輯OR運算子(Logical OR Operator)
    x|y
  11. 條件式AND運算子(Conditional AND Operator)
    x&&y
  12. 條件式OR運算子(Conditional OR Operator)
    x||y
  13. Null聯合運算子(Null-coalescing Operator)
    x??y
  14. 條件運算子(Conditional Operator)
    t?x:y
  15. 指派及Lambda運算子(Assignment and Lambda Operators)
    x=y, x+=y, x-=y, x*=y, x/=y, x%=y, x&=y, x|=y, x^=y,x<<=y,x>>=y,=>
  16. 算術溢位(Arthmetic Overflow)

??排名第13,故 lastQty - soldQty ?? 0 會先計算 100-null 再取??0,由結果反推 100-null 的結果為null。

null與數字如何運算

MSDN文件

The predefined unary and binary operators and any user-defined operators that exist for value types may also be used by nullable types. These operators produce a null value if the operands are null; otherwise, the operator uses the contained value to calculate the result.

當運算元(即運算子範圍中的x或y)型別為Nullable<ValueType>且其值為null,以一元或二元運算子計算結果恆為null。

When performing comparisons with nullable types, if one of the nullable types is null, the comparison is always evaluated to be false. It is therefore important not to assume that because a comparison is false, the opposite case is true

對null進行大小比較時,其結果恆為false,例如:int? value = null,則條件式 value > 0、 value < 0、value == 0 都不成立。

此一特性跟 DB 的 null 幾乎一模一樣,未來應用時要留意。(延伸閱讀:詭異的NOT IN查詢,原來是NULL搞鬼

又上了一課~

【2016-07-14補充】

在專頁陸續接獲網友Shengkai及比爾叔補充好建議一則:面對此類情境索性加上括號寫成:leavQty – (soldQty ?? 0),一來語意清楚方便後續維護者理解,二來免除記錯運算子優先順序的風險,決定未來都依此辦理。

Dapper小技巧:以資料表保存集合物件JSON

$
0
0

專案常遇到的需求:為指定資料保留修改歷程,以備稽核檢查或追查責任之用,使用機率不高且無統計或隨興查詢需求,不值得另開資料表。此時我偏好的做法是定義成List<HistoryRecord>,在資料表開一個NVARCHAR(MAX)保存其JSON內容,調閱時讀取JSON反序列化還原內容,足以滿足規格所需。

用個實例說明,假設資料物件定義如下:

publicclass HistoryRecord
{
public DateTime Time { get; set; }
publicstring User { get; set; }
publicstring Remark { get; set; }
}
 
publicclass ProjectItem
{
publicint Id { get; set; }
publicstring Name { get; set; }
public List<HistoryRecord> History { get; set; }
}

資料表設計如下:

CREATETABLE [dbo].[ProjectItem] (
    [Id]      INTNOTNULL,
    [Name]    NVARCHAR (64)  NULL,
    [History] NVARCHAR (MAX) NULL
);

資料庫存取部分我主要用Dapper實作,但問題來了,試著將List<HistoryRecord>當成History欄位的輸入參數:

var kernel = new ProjectItem()
{
    Id = 1,
    Name = "KernelModule",
    History = new List<HistoryRecord>()
    {
new HistoryRecord()
        {
            Time = new DateTime(2016, 7, 1),
            User = "Jeffrey",
            Remark = "Initial version"
        },
new HistoryRecord()
        {
            Time = new DateTime(2016,7,11),
            User = "Jeffrey",
            Remark = "Refactoring"
        }
    }
};
cn.Execute("INSERT INTO ProjectItem VALUES(@Id, @Name, @History)", kernel);

卻冒出錯誤:The member  of type DapperLab.Program+HistoryRecord cannot be used as a parameter value。Dapper不知道怎麼將List<HistoryRecord>轉成可以存入資料庫的內容。

針對這類需求,Dapper的解決方案是開放開發者自訂TypeHandler,指定型別該如何對應資料庫內容。實作方法很簡單,宣告一個類別繼承SqlMapper.TypeHandler<T>,提供兩個函式:Parse<T>()函式負責將資料庫內容轉成該型別,SetValue()函式將型別轉型後指定給IDbDataParameter.Value:

publicclass HistoryRecordListHandler : SqlMapper.TypeHandler<List<HistoryRecord>>
{
publicoverride List<HistoryRecord> Parse(objectvalue)
    {
return JsonConvert.DeserializeObject<List<HistoryRecord>>((string)value);
    }
 
publicoverridevoid SetValue(IDbDataParameter parameter, List<HistoryRecord> value)
    {
        parameter.Value = JsonConvert.SerializeObject(value);
    }
}

接著,在執行cn.Execute()之前先透過SqlMapper.AddTypeHandler()註冊,指定由HistoryRecordListHandler負責處理List<HistoryRecord>資料轉換:
SqlMapper.AddTypeHandler<List<HistoryRecord>>(new HistoryRecordListHandler());

如此,Dapper就會將Historyh屬性JSON後寫入資料表,讀取時也能正確由JSON還原回List<HistoryRecord>,大功告成!

以上寫法可以再改良,HistoryRecordListHandler的核心邏輯可抽取成泛型,適用於所有要JSON化存入資料庫的型別,省去為每個要轉JSON型別撰寫專屬TypeConverter的困擾。

publicclass JsonConvertHandler<T> : SqlMapper.TypeHandler<T>
{
publicoverride T Parse(objectvalue)
    {
return JsonConvert.DeserializeObject<T>((string)value);
    }
 
publicoverridevoid SetValue(IDbDataParameter parameter, T value)
    {
        parameter.Value = JsonConvert.SerializeObject(value);
    }
}
 
//...略...
 
SqlMapper.AddTypeHandler<List<HistoryRecord>>(new JsonConvertHandler<List<HistoryRecord>>());

Dapper筆記:列舉轉VARCHAR研究

$
0
0

一個用資料表保存C# Model的常見問題,列舉型別屬性該怎麼處理?

例如有個BlogUser資料物件,包含Id、Name及Role三個屬性,其中Role是列舉,包含Admin、Editor、Blogger、Reader等項目。保存BlogUser的資料表設計如下,Role欄位定義為VARCHAR(8),目標為直接保存"Admin"、"Blogger"等字串內容,以期在SQL可使用WHERE Role = 'Blogger'進行篩選。

CREATETABLE [dbo].[BlogUser] (
    [Id]   INTNOTNULL,
    [Name] VARCHAR (16) NOTNULL,
    [Role] VARCHAR (8)  NOTNULL,
CONSTRAINT [PK_BlogUser] PRIMARYKEYCLUSTERED ([Id] ASC)
);

使用Dapper執行資料更新及查詢的程式範例如下:

using Dapper;
using System;
using System.Data.SqlClient;
using System.Linq;
 
namespace DapperLab
{
class Program
    {
staticstring cnStr = "...由config取得連線字串(記得要加密),此處省略...";
 
publicenum Roles
        {
            Admin,
            Editor,
            Blogger,
            Reader
        }
 
publicclass BlogUser
        {
publicint Id { get; set; }
publicstring Name { get; set; }
public Roles Role { get; set; }
        }
 
staticvoid Main(string[] args)
        {
using (var cn = new SqlConnection(cnStr))
            {
                var jeff = new BlogUser()
                {
                    Id = 1,
                    Name = "Jeffrey",
                    Role = Roles.Blogger
                };
                cn.Execute("INSERT INTO BlogUser VALUES(@Id, @Name, @Role)", jeff);
                var data = cn.Query<BlogUser>(
"SELECT * FROM BlogUser WHERE Id = @Id",
new { Id = 1 }).Single();
                Console.WriteLine("{0} {1} {2}", data.Id, data.Name, data.Role);
            }
        }
    }
}

測試結果Role列舉可以被寫入資料庫並正確還原,但Role欄位寫入的是Blogger列舉項目對應的數值"2"。

實測若在Role欄位存入'Blogger'也能正確還原回Roles.Blogger,但寫入時只能寫入數字讓人頭大。研究很久,一直試不出用列舉項目名稱取代數值寫入資料庫的做法。

昨天曾介紹過SqlMapper.TypeHandler<T>自訂轉換邏輯技巧,可惜無法適用列舉型別。由Dapper原始碼SqlMapper.cs邏輯,發現Dapper一旦偵測出IsEnum(),會無視TypeHandler設定直接使用Enum.ToObject()。

privatestatic T Parse<T>(objectvalue)
{
if (value == null || valueis DBNull) returndefault(T);
if (valueis T) return (T)value;
    var type = typeof(T);
    type = Nullable.GetUnderlyingType(type) ?? type;
if (type.IsEnum())
    {
if (valueisfloat || valueisdouble || valueisdecimal)
        {
value = Convert.ChangeType(value, Enum.GetUnderlyingType(type), 
                    CultureInfo.InvariantCulture);
        }
return (T)Enum.ToObject(type, value);
    }
    ITypeHandler handler;
if (typeHandlers.TryGetValue(type, out handler))
    {
return (T)handler.Parse(type, value);
    }
return (T)Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
}

關於Enum該不該支援TypeHandler範圍,Github上有不少相關討論並無共識,預期短期內此一行為不會有所改變。(查看原始碼時,意外發現Dapper竟動用ILGenerator動態組裝MSIL處理欄位對應,相當變態,也難怪執行效能讓其他Reflection競爭者看不到車尾燈)

找不到克服之道也不想修改Dapper核心,最後我採取的做法是另外宣告一個RoleText屬性,提供以字串讀取及設定Role屬性的管道,其值與Role列舉100%對應,至於資料表欄位則改為RoleText VARCHAR(16)。程式範例如下:

publicclass BlogUser
        {
publicint Id { get; set; }
publicstring Name { get; set; }
public Roles Role { get; set; }
 
            [JsonIgnore]
publicstring RoleText {
                get
                {
return Role.ToString();
                }
                set
                {
                    Roles res;
if (!Enum.TryParse((string)value, out res))
                    {
thrownew ApplicationException(string.Format(
"Can't convert '{0}' to type [{1}]", value, typeof(Roles)));
                    }
                    Role = res;
                }
            }
        }

以上是我處理Dapper儲存列舉型別的經驗供參,大家如果知道其他妙計,歡迎回饋!

使用Oracle資料表保存GUID屬性

$
0
0

大家都知道我隸屬GUID PK幫 .NET分舵,最近寫了個小模組,Model理所當然地使用GUID當作Primary Key,由於想同時支援SQL Server跟Oracle,第一次挑戰SQL跟Oracle共用Model。先前的GUID PK經驗都在SQL,SQL有Uniqueidentifier型別,跟C#端的Guid型別能整到天衣無縫;同樣的情場搬到Oracle,就需要動點腦筋解決。

歷經一番摸索,心得如下:

該用什麼型別?

Oracle沒有Uniqueidentifier可用,我心中的兩個選項是CHAR(32)或是RAW(16)。

CHAR(32)比較直覺,手工SQL查詢時可以直接寫WHERE SomeKey = 'd467c0d30d5b44fdb38fe3275685e43e',但CHAR(32)長度足足是RAW(16)的兩倍,撇開資料儲存空間不談,若考慮Index效能,30公分32 Byte與16 Byte的差別很難被無視。基於效能理由,我決定使用RAW(16),至於手工查詢時可以用HEXTORAW()RAWTOHEX()搞定,寫成WHERE SomeKey = HEXTORAW('d467c0d30d5b44fdb38fe3275685e43e'),不算複雜。

GUID與RAW(16)轉換

以C#的角度,RAW(16)等同byte[16],Guid.ToByteArray()可將Guid轉為byte[16],而new Guid(byte[])則可將byte[16]轉為Guid,雙向轉換易如反掌,但隱藏一個問題。例如:

            Guid g = Guid.NewGuid();
            Console.WriteLine("Orig Guid : {0}", g);
byte[] b = g.ToByteArray();
            Console.WriteLine("ToByteArray : {0}", BitConverter.ToString(b));
            Guid r = new Guid(b);
            Console.WriteLine("new Guid(byte[]) : {0}", r);
string s = g.ToString("N");
            r = new Guid(s);
            Console.WriteLine("new Guid(\"{0}\") : {1}", s, r);

執行結果:           
Orig Guid : d467c0d3-0d5b-44fd-b38f-e3275685e43e
ToByteArray : D3-C0-67-D4-5B-0D-FD-44-B3-8F-E3-27-56-85-E4-3E
new Guid(byte[]) : d467c0d3-0d5b-44fd-b38f-e3275685e43e
new Guid("d467c0d30d5b44fdb38fe3275685e43e") : d467c0d3-0d5b-44fd-b38f-e3275685e43e

Guid先轉成byte[],用new Guid(byte[])就能再轉回一模一樣的Guid,但仔細一看,byte[]的位元組順序與xxxxxxxx-xxxx-…寫法不同,d467c0d3-0d5b-44fd-b38f-e3275685e43e被轉成"D3-C0-67-D4"-"5B-0D"-"FD-44"-"B3-8F"-E3-27-56-85-E4-3E,其中d467c0d3、0d5b、44fd的位元組順序前顛倒(採Little Endian),b38f-e3275685e43eb部分則順序相同,一切是Guid規格使然。

理論上只要byte[]能再還原成原本的Guid值,倒也不需要去堅持byte[]儲存的順序。但依實務經驗,這個順序差異一定會帶來困擾。例如,在Oracle SELECT取得RAW(16)內容直接複雜貼上當成URL參數是偵錯時常見的場景,若RAW(16)儲存的是ToByteArray()的內容,交給C#用new Guid("D10E1D815C2340F98FDAF3656C237E5C")會變成d10e1d81-5c23-40f9-8fda-f3656c237e5c,而當初存入的811d0ed1-235c-f940-8fda-f3656c237e5c。

為克服Guid.ToByteArray()結果無法直接以字串方式轉成Guid,我借用Stackoverflow上網友分享的轉換函式做成ToRaw16()與FromRaw16()方法,藉此確保C#的Guid表示字串與Oracle RAW(16)查詢結果一致,以利偵錯。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Afet.Attachment
{
publicstaticclass GuidConverter
    {
//REF: http://stackoverflow.com/a/17168469/4335757
/// <summary>
/// A CLSCompliant method to convert a big-endian Guid to little-endian
/// and vice versa.
/// The Guid Constructor (UInt32, UInt16, UInt16, Byte, Byte, Byte, Byte,
///  Byte, Byte, Byte, Byte) is not CLSCompliant.
/// </summary>
publicstatic Guid FlipEndian(this Guid guid)
        {
            var newBytes = newbyte[16];
            var oldBytes = guid.ToByteArray();
 
for (var i = 8; i < 16; i++)
                newBytes[i] = oldBytes[i];
 
            newBytes[3] = oldBytes[0];
            newBytes[2] = oldBytes[1];
            newBytes[1] = oldBytes[2];
            newBytes[0] = oldBytes[3];
            newBytes[5] = oldBytes[4];
            newBytes[4] = oldBytes[5];
            newBytes[6] = oldBytes[7];
            newBytes[7] = oldBytes[6];
 
returnnew Guid(newBytes);
        }
 
publicstaticbyte[] ToRaw16(this Guid guid)
        {
return guid.FlipEndian().ToByteArray();
        }
 
publicstatic Guid FromRaw16(byte[] raw)
        {
returnnew Guid(raw).FlipEndian();
        }
 
    }
} 

 

SQL與Oracel共用Model

使用Dapper時,Model的Guid屬性與SQL Uniqueidentifier能自動對應轉換,在Oracle卻會遇上麻煩,cn.Query("… WHERE SomeRaw16Col = :d", new { id = Guid.NewGuid() })會導致以下錯誤:

System.ArgumentException was unhandled; Message=Value does not fall within the expected range.Source=Oracle.ManagedDataAccess.

Stackoverflow找到網友自訂OracleGuid型別再配合先前提過的SqlMapper.TypeHandler<T>完成轉換。考量將Guid型別換成古怪的OracleGuid會讓其他開發團隊成員迷惑,二則面對一些依型別進行不同處理的邏輯,增加自訂型別將造成困擾,第三,這個Mode為SQL與Oracle共用,還得留意SQL及Oracle使用不同TypeHandler設定的陷阱。

最後,我採行與處理列舉轉VARCHAR相同的做法,另外增加一個byte[] IdRaw欄位,與Guid Id 100%對應,面對SQL時用Uniqueidentifier對應Guid Id,遇到Oracle時則改用RAW(16)對應byte[] IdRaw。範例如下:

/// <summary> 
/// 附件容器 
/// </summary> 
publicclass AttachmentContainer 
    { 
/// <summary> 
/// 附件所屬項目識別碼 
/// </summary> 
public Guid OwnerId { get; set; }
 
/// <summary> 
/// 附件所屬項目識別碼(Byte[]版本) 
/// </summary> 
        [JsonIgnore] 
publicbyte[] OwnerIdRaw { 
            get 
            { 
return OwnerId.ToRaw16(); 
            } 
             set 
            { 
                OwnerId = GuidConverter.FromRaw16(value); 
            } 
        } 
 
        
        
以上就是在Oracle資料表儲存GUID的一些小技巧,提供大家參考。
Viewing all 2311 articles
Browse latest View live