Quantcast
Channel: Darkthread
Viewing all 428 articles
Browse latest View live

網頁內嵌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),一來語意清楚方便後續維護者理解,二來免除記錯運算子優先順序的風險,決定未來都依此辦理。

【2016-07-19補充】

讀者Ken補充Nullable<T>有個好用函式GetValueOrDefault(),soldQty ?? 0 可寫成 soldQty.GetValueOrDefault(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的一些小技巧,提供大家參考。

【茶包筆記】 Visual Studio遇web.config鎖定無法覆寫

$
0
0

最近遇到兩次,特筆記備忘。

在Windows 8.1使用Visual Studio 2015偵錯ASP.NET網站,修改web.config後存檔,出現被其他程序佔用無法存取錯誤。

The process cannot access the file '…web.config' because it is being used by another process.
無法存取檔案 '…web.cofig',因為其他處理序正在使用此檔。

優先猜想web.config是被IIS Express鎖定,嘗試從工具列停止站台,問題依舊。

換個方法,在檔案總管將檔案更名想確認是否web.config真被鎖定無法更動,意外得到更明確的錯誤訊息,Windows直接供出嫌犯是Microsoft.VisualStudio.Web.Host.exe。

利用工具管理員將Microsoft.VisualStudio.Web.Host.exe排除,問題排除。

事後在Stackoverflow查到類似討論(有VS2015+Windows 10案例)但無明確結論,以強制中止Microsoft.VisualStudio.Web.Host.exe為暫時解法。

另外本次學到一招:遇到檔案被鎖住,可先改從檔案總管更名,有可能Windows會直接回報鎖定來源,省去動用ProcessExplorer、Unlocker… 等工具的手續。


關於IE快取更新檢查設定

$
0
0

接獲報案,某使用者今天送出的ASP.NET表單,有某個應為隨機Guid<input type="hidden">欄位,內容竟與幾週前送出的資料重複,因而導致錯誤。

推測最大可能是使用到被IE快取的舊內容導致,查看使用者的IE設定,登楞!

竟被設定「永不」檢查是否有較新版本。經實測,一旦調成此設定,就算重開IE,連上ASP.NET網頁裡的Hidden欄位是上次的舊內容,要等到按F5或重新網頁才會更新。

由此推測問題出在使用者設定了「只要有Cache,永不檢查新版本」,而ASP.NET未防止Cache,因而產生問題。

不過,畫面中的四個選項有何不同,我還真沒認真研究過,藉此機會整理MS KB對「檢查儲存的畫面是否有較新的版本」選項的說明並加上我自己的詮釋:

  • 每次造訪網頁時
    每次連上網頁都重新檢查是否內容有更新,若有更新就顯示新網頁並快取內容。
    背後原理照常送出HTTP Request,而在Header透過If-Modified-Since或If-None-Match傳送上次取得內容時間或ETag,伺服器端依此判斷,若內容已變動則以HTTP 200傳回更新內容,否則傳回HTTP 304 Not Modifed告知瀏覽器使用快取內容。
    此選項不易誤用過期內容,但也不會減少下載Request數(遇HTTP 304資料傳輸量會減少),效率較差。
  • 每次啟動Internet Explorer時:
    在重啟IE之前,重複造訪相同網頁將直接使用快取內容(省略發出Request詢問伺服器是否有更新),按下F5或重新整理時則會重新由網站下載。重啟IE後,造訪相同網頁會檢查是否有新內容。
  • 自動:
    與上一選項相似,但加入額外邏輯演算法偵測網頁中圖片等靜態項目的更新頻率,降低不常變動者的檢查新內容的頻率(即使重啟IE也不會檢查新內容)。
  • Never:
    從不檢查新內容,一律使用快取,除非使用者按下F5或重新整網頁。

由此可知,除非選擇「每次造訪網頁時」,IE在存取ASP.NET網頁時,都有可能直接取用Cache內容而不是重新執行ASP.NET程式,若網頁中有某些每次開啟都不同的隨機內容,需使用一些技巧避免因取用Cache內容生錯。最簡單的做法是設定No-Cache,ASP.NET程式可加入Response.Cache.SetCacheability(HttpCacheability.NoCache);禁止網頁被Cache,在ASP.NET MVC可用[OutputCacheAttribute(VaryByParam = "*", Duration = 0, NoStore = true)][參考];另一個思考方向則是使用JavaScript,載入網頁後再更新或檢查隨機內容。

總之,設計網站時一定要防範使用者「使用Cache內過時內容送出表單或進行交易」,AJAX及SPA程式寫多了常就疏忽掉這點,本起案例讓我重新喚起警覺,筆記之。

小技巧:在web.config加入多筆式設定

$
0
0

跟同事聊到如何在web.config加入多筆式設定。所謂多筆式設定,是指同性質設定可能有1到n筆並存,我常遇到的例子是偵錯用途或排除例外的對應設定,例如:將Windows登入帳號A對應成帳號B,部門C對應成部門D… 等等。這類設定,若筆數很多我通常會另外弄個Text或JSON保存,若筆數不多只有三五筆,我喜歡直接寫進config檔,比較乾淨俐落。

舉個實例,假設有個整合式驗證ASP.NET網站依登入帳號識別使用者身分,帳號manager具有管理權限。開發人員jeffrey臨時想模擬manager登入進行測試,當然不能跑去跟主管講「可不可以給我你的AD帳號密碼?」。我慣用的簡單解法是在系統加入一小段額外邏輯,由web.config讀取設定,允許將某些帳號對應成其他帳號。開發測試人員jeffrey可使用自己的帳號登入,由系統將其轉換成manager身分。測試完畢再移除設定,恢復以jeffrey身分操作系統。

要加入設定,大家最先想到的一定是<appSettings>,但<add key="…" value="…" />設定以key值識別,適合一種設定一筆,當資料有多筆就要想辦法合併或編碼。如果是字串陣列還簡單,可靠CSV搞定,例如:<add key="AdminUsers" value="jeffrey,darkthread" />。但若遇到需要兩個值的對應設定就得自訂編碼法則,例如:<add key="AccountMapping" value="jeffrey:manager;darkthread:admin" />,讀取時得解碼,但感謝有LINQ,我們用一行就可搞定:

(ConfigurationManager.AppSettings["AccountMapping"] ?? "").Split(';')
.Select(o => o.Split(':')).ToDictionary(o => o[0], o => o[1]);

但以上做法有個小缺點,資料筆數一多value值會變得很長很亂,修改起來容易眼花,而且要留意分隔符號出現在設定值裡的可能性。

後來我想到一種自認不錯的解法,指定專屬前置詞(例如:"mapping:"),寫成:

<appSettings>
<addkey="mapping:jeffrey"value="manager"/>
<addkey="mapping:darkthread"value="admin"/>
</appSettings>

一筆設定寫成一行,閱讀或修改都很清楚明瞭,而要取值也很簡單,再次交由神奇的LINQ搞定:

ConfigurationManager.AppSettings.AllKeys.Where(o => o.StartsWith("mapping:"))
.ToDictionary(o => o.Split(':').Last(), o => ConfigurationManager.AppSettings[o]);

今天介紹的這個做法很方便好用吧?我們下次再見… (揮手下降,但馬上被人拖上來)

謎之聲:等等!在config中使用多筆式設定,明明.NET就有提供強型別化的正規寫法,你老是教別人這類取巧的旁門左道像話嗎?你的社會責任呢?

呃… 好的。現在來介紹如何自訂ConfigurationSection,在web.config或App.config使用自訂XML元素名稱優雅地加入多筆式設定,像這樣:

<accountMapping>
<mappings>
<addfrom="jeffrey"to="manager"></add>
<addfrom="darkthread"to="admin"></add>
</mappings>
</accountMapping>

首先,我們要在程式裡定義accountMapping對應的ConfigurationSection型別,mappings對應的ConfigurationElementCollection型別,以及具備from與to兩個屬性的ConfigurationElement型別。程式範例如下:

using System.Configuration;
 
namespace ConfigTest
{
//REF: http://www.abhisheksur.com/2011/09/writing-custom-configurationsection-to.html
 
publicclass Mapping : ConfigurationElement
    {
        [ConfigurationProperty("from", IsRequired = true)]
publicstring From { get { returnbase["from"].ToString(); } }
        [ConfigurationProperty("to", IsRequired = true)]
publicstring To { get { returnbase["to"].ToString(); } }
    }
 
    [ConfigurationCollection(typeof(Mapping))]
publicclass MappingCollection : ConfigurationElementCollection
    {
protectedoverride ConfigurationElement CreateNewElement()
        {
returnnew Mapping();
        }
protectedoverrideobject GetElementKey(ConfigurationElement element)
        {
return (element as Mapping);
        }
    }
 
 
publicclass AccountMappingSection : ConfigurationSection
    {
        [ConfigurationProperty("mappings")]
public MappingCollection Mappings
        {
            get { return ((MappingCollection)(base["mappings"])); }
            set { base["mappings"] = value; }
        }
    }
 
}

寫好後,記得要在web.config中加入自訂configurationSection項目,如此就能在config使用accountMapping/mappings加入設定:

<configuration>
<configSections>
<sectionname="accountMapping"type="ConfigTest.AccountMappingSection, ConfigTest"/>
</configSections>
<accountMapping>
<mappings>
<addfrom="jeffrey"to="manager"></add>
<addfrom="darkthread"to="admin"></add>
</mappings>
</accountMapping>
</configuration>

讀取時,使用ConfigurationManager.GetSection()取回設定內容進行型別轉換,可以強型別方式取得設定:

            AccountMappingSection sec = 
                ConfigurationManager.GetSection("accountMapping") 
as AccountMappingSection;
if (sec != null)
            {
foreach (Mapping mapping in sec.Mappings)
                {
                    Console.WriteLine("{0} -> {1}", mapping.From, mapping.To);
                }
            }

當設定內容不合規範,會有明確的ConfigurationErrorsExceptoin錯誤訊息,非常清楚。

自訂ConfigurationSection能徹底做到條理分明,一絲不苟!但代價是必須定義一堆囉嗦的自訂型別。當設定項目很多且龐雜時,採取嚴謹做法有其必要性,但像文章開頭的範例只想條列幾筆同性質設定,值不值得擺出此等陣仗,大家就自行拿捏囉~

Entity Framework筆記:使用Oracle Synonym

$
0
0

遇到EF使用Oracle Synonym問題,查了資料做了實驗,整理筆記如後。

先說我們在Oracle使用Synonym(別名,有人翻成「同義詞」,我覺得別名順口)的情境:例如人事系統使用"HR"帳號登入Oracle並在自己的HR Schema建立資料表並擁有HR Schema所有資料表的讀寫權限。之後ERP系統要讀存HR Schema下的員工基本資料,當然不能直接用HR帳號密碼連Oracle,而會另開HRQRY之類的帳號,再授與其HR.Employee資料表的讀取權限。這種場景在SQL Server也很常見,只需連線字串改用HRQRY帳號登入,Initial Catalog改指向HR資料庫,用SELECT * FROM Employee就能查到資料。Oracle的Schema概念有點不同,沒法在連線字串用Initial Catalog指定Schema,HRQRY要存取HR下的資料表,得寫成SELECT * FROM HR.Employee。這樣做有兩個缺點,一是每次動用Table時都要加上"HR."挺囉嗦,二是在程式碼寫死了Schema名稱,一旦Schema名稱調整將有改不完的程式。簡便的解決之道是為HR.Employee建立Synonym:
CREATE PUBLIC SYNONYM Employee FOR HR.Employee;

如此,以HRQRY登入也能用SELECT * FROM Employee查詢HR.Employee,如要改由其他Schema讀取Employee,只需重設Synonym,不必改程式。

不過這個做法搬到Entity Framework會遇到一點小挑戰!在Visual Studio建立EF模型時,Entity Data Model Wizard/Choose Your Database Objects(操作畫面請參考舊文) 看不到任何Synonym資料表,換句話說,我們無法使用HRQRY帳號為Synonym資料表建立資料模型!

爬文找到CodePlex EF專案的一則討論,開發團隊基於應用需求不多的理由,未將Synonym納入Entity Data Model Wizard的找尋範圍,這就是操作介面看不到Synonym資料表、檢視、Stored Procedure的原因。

要解決問題,有兩個選擇:

  1. 開發階段以原帳號(HR)連線資料建立資料模型,測試及上線時再改用HRQRY。
  2. 如果能接受Code First開發模式(延伸閱讀),OnModelCreating()事件允許指定Schema名稱,如此連Synonym都不需建立。

我們目前開發上已習慣EDMX的視覺化呈現,暫時沒有轉往Code First的打算,故選擇第一種做法。而依據實測,先用HR建好模型,只要Synonym名稱齊全,與原資料表名稱一致且權限有開,連線字串的登入帳號由HR換成HRQRY即可無縫接軌,不需更動任何程式碼,還算方便。

爬文期間還查到一篇Synonym名稱與原資料表不同,如何修改EDMX配合的Hacking技巧,一併記下備忘。

NG筆記28-Checkbox清單進化版

$
0
0

很久以前就寫過Angular版的Checkbox清單,不過當時的版本有點簡陋,只能以字串陣列作為來源。我心目中的理想Checkbox清單元件,應該要像ng-options能用物件陣列當作資料來源,最好還可以切換單選模式(我知道改用Radio就能單選,但規格書不時出現註明要單選的Checkbox清單),沒找到前人寫好的現成作品,那就自己刻一個吧!
(「花更多時間去找元件」 vs 「把時間省下來自己寫一個」 常讓人左右為難,尤其當技難度不高,有時找輪子耗費的心力比造輪子還多!)

範例:

$scope.ObjItems = [
    { k: "A1", name: "Jeffrey" },
    { k: "A2", name: "Darkthread" },
    { k: "B1", name: "Hacker" }
];
$scope.StrArray = ["Jeffrey", "Darkthread", "Hacker"];
<divafet-cbx-lista-items="ObjItems"a-model="SelObjs"a-text-field="name"></div>
<divafet-cbx-lista-items="StrArray"a-model="SelString"a-exclusive="true"></div>

參數說明:

  • a-items 
    產生勾選方格清單的資料來源,可以是物件陣列或字串陣列。
  • a-model 
    要繫結選取結果的屬性,在多選模式其型別為物件或字串陣列,在單選模式則為物件或字串。
  • a-text-field 
    當資料來源為物件指定,需使用a-text-field決定以哪個屬性做為顯示在清單上的文字。
  • a-explusive 
    預設為多選模式,可透過a-exclusive="true"切換成單選模式。  

我做了Live Demo讓大家玩,完整程式碼已放上github,有需要者請自取。

[NG系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

NG筆記29-下拉選單連動

$
0
0

跟同事討論到下拉選單連動(最常見的經典應用是縣市、行政區下拉選單連動,選取縣市後自動換成該縣市的行政區清單),這才發現針對這門必修課,我只寫過KO版範例,沒寫過NG版,趕緊補上。

我寫了一個三層式下拉選單連動範例,在ViewModel中安排Level1、Level2、Level3三個屬性保存下拉選單選取結果,另外用L1Options、L2Options、L3Options分別存放Level1-3的下拉選單選項。透過$scope.$watch(),在Level1變動時更新第二層選項,在Level1或Level2變動時更新第三層選項。更新選項時,若Level2/Level3的值不在選項中,則自動切到第一個選項。

為驗證反向操作,我還做了一個修改Level1、Level2、Level3值的按鈕,測試修改資料後下拉選單是否能正確對應。

<!DOCTYPEhtml>
<htmlng-app="app">
<head>
<metacharset="utf-8">
<metaname="viewport"content="width=device-width">
<title>linked dropdowns</title>
<style>
    body { font-size: 9pt; }
    div { padding: 6px; }
</style>
</head>
<bodyng-controller="main">
<div>
<selectng-model="m.Level1"ng-options="o as o for o in m.L1Options"></select>
<selectng-model="m.Level2"ng-options="o as o for o in m.L2Options"></select>
<selectng-model="m.Level3"ng-options="o as o for o in m.L3Options"></select>
</div>
<div>
    L1 = {{m.Level1}}, L2 = {{m.Level2}}, L3 = {{m.Level3}}
</div>
<div>
<selectng-model="m.Path"ng-options="o as o for o in m.PathOptions"></select>
<buttonng-click="m.SetLevels()">Set Levels</button>
</div>
<scriptsrc="https://code.jquery.com/jquery-3.0.0.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<script>
function myViewModel(scope) {
var self = this;
        self.Level1 = null;
        self.Level2 = null;
        self.Level3 = null;
//模擬資料
var data = self.Data = {
"台北": {
"文山": [ "政大" ],
"大安": [ "台大", "台科大" ]
          },
"新竹": {
"東區": [ "交大", "清大" ]
          },
"台南": {
"東區": [ "成大" ],
"官田": [ "南藝" ]
          }
        };
//各Level對應的選項集合
        self.L1Options = Object.keys(self.Data);
        self.Level1 = self.L1Options[0];
        self.L2Options = [];
        self.L3Options = [];
//Level1變更時連動L2Options
        scope.$watch("m.Level1", function() {
            self.L2Options = data[self.Level1] ? Object.keys(data[self.Level1]) : [];
//檢查Level2是否在選項中,若無將Level2設定第一筆選項
var idx = $.inArray(self.Level2, self.L2Options);
if (idx == -1) self.Level2 = self.L2Options[0];
        });
//Level1或Level2變更時連動L3Options
        scope.$watch("m.Level1+'/'+m.Level2", function() {
            self.L3Options = 
                data[self.Level1] && data[self.Level1][self.Level2] ?
                data[self.Level1][self.Level2] :
                [];
//檢查Level3是否在選項中,若無將Level3設定第一筆選項
var idx = $.inArray(self.Level3, self.L3Options);
if (idx == -1 ) self.Level3 = self.L3Options[0];
        });
//產生單層資料,形成下拉選單,用來測試更動Level1/Level2/Level3後連動是否正確
var list = [];
        self.L1Options.forEach(function(city) {
            Object.keys(data[city]).forEach(function(area) {
                data[city][area].forEach(function(school) {
                    list.push(city + "/" + area + "/" + school);
                });
            });
        });
        self.Path = "";
        self.PathOptions = list;
//按鈕後修改Level1/Level2/Level3
        self.SetLevels = function() {
var p = self.Path.split('/');
            self.Level1 = p[0];
            self.Level2 = p[1];
            self.Level3 = p[2];
        };
      }      
      angular.module("app", [])
      .controller("main", function ($scope) {
        $scope.m = new myViewModel($scope);
      });
</script>
</body>
</html>

在實務上,選項可能需要透過AJAX方式取回,此時將兩個$watch()函式改為AJAX查詢邏輯即可。JSBin上有Live Demo,大家可以動手玩玩。

[NG系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs
Viewing all 428 articles
Browse latest View live