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

【茶包射手日記】ASP.NET網站bindingRedirect無效

$
0
0

故事從某個Windows 2003上的ASP.NET 3.5網站搬到Windows 2012 R2說起,移至新主機後蹦出以下訊息:

Could not load file or assembly 'System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. The system cannot find the file specified.

這問題可難不倒熟悉ASP.NET歷史的老骨頭,System.Web.Extensions 1.0.6來自AJAX Control Toolkit,是ASP.NET 2.0時代要實現AJAX功能的套件,到ASP.NET 3.5時納為標準配備不需另外安裝,但版本升級為3.5。這個網站從ASP.NET 2.0一路開發,後來才升級到3.5,AJAX套件一直運作良好沒理由調整,直到搬遷新主機少了AJAX Control Toolkit才發生問題。

要解決問題有兩個方法:在Windows 2012 R2安裝AJAX Control Toolkit(這… 太Low了,拎杯做不下手),或是透過bindingRedirect將1.0.6繫結重新導向3.5版。

<runtime>
<assemblyBindingappliesTo="v2.0.50727">
<dependentAssembly>
<assemblyIdentityname="System.Web.Extensions"publicKeyToken="31bf3856ad364e35"/>
<bindingRedirectoldVersion="1.0.0.0-1.1.0.0"newVersion="3.5.0.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentityname="System.Web.Extensions.Design"publicKeyToken="31bf3856ad364e35"/>
<bindingRedirectoldVersion="1.0.0.0-1.1.0.0"newVersion="3.5.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>

熟門熟路調好設定準備手到擒來… 噹!踢到鐵板,錯誤訊息依舊。

反覆檢查config設定,張大眼睛撐到眼眶發也看不出異常。不得已使出大絕-愚公移山法,找到另一個類似背景但在該主機上可以正常運作的web.config逐行比對,將可疑之處一行一行調到跟可運作版一致。最後,關鍵竟在想也想不到的地方:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

將xmlns移除,問題就消失了!

找到關鍵後爬文,發現這是曾讓不少人摔坑的經典問題(參考 12 3),好發於舊版ASP.NET網站,我實測用VS2013/VS2015建立ASP.NET 4網站,<configuration>已無任何Namespace宣告。至於為什麼加上xmlns會導致bindingRedirect失效?爬完文仍未得解,留下結論:

遇到bindingRedirect設定無效時,請檢查是否<configuration>是否多了xmlns設定。


XLS轉XLSX研究

$
0
0

要在C#讀寫Excel檔,直接呼叫Excel.exe是最直覺功能最齊全的做法,但Excel屬桌面互動程式,透過Web或排程等背景程序執行常有問題要克服,同時,只為單純讀取資料招喚龐大笨重的Excel程式有殺雞用牛刀之嫌,第三方元件是更理想的方式。

過去用過多套Excel處理程式庫:NPOIEPPlusOpenXML SDKClosedXML,一路下來,ClosedXML支援不少Excel VBA風格的簡潔API,檔案相容性測試又比NPOI及EPPlus好,成為我處理Excel的首選。但有個問題,ClosedXML基於OpenXML SDK,只支援xlsx格式,遇上必須支援xls的場合,只能回歸NPOI。

用慣了ClosedXML,不甘心回頭用NPOI,心生一念,何不先將xls轉成xlsx再用ClosedXML讀取?於是,我展開尋找xls轉xlsx方法的旅程。

  1. 線上轉換服務
    網路有一些線上轉檔服務,例如:ZAMZARConvertio,提供各式檔案格式轉換,xls只是其中之一,這些線上服務有API可供程式整合,免費付費都有。但線上轉換有個致命缺點,上傳內部文件到Internet有資安疑慮,直接領便當。
  2. Office Migration Planning Manager
    OMPM是升級Office 2007/2010的規劃工具,內附一個轉檔工具:Office File Converter (OFC),能將doc、xls、ppt轉成docx、xlsx、pptx格式。使用前必須安裝Microsoft Office相容性套件,但由於原本被拿來批次轉換,ofc.exe不接受參數,要先將檔案放在特定資料夾並修改ofc.ini設定才能轉換。實測我一直卡在"failed to convert"錯誤,轉換失敗。 
    X:\Tools\OMPM\TOOLS>ofc
    Microsoft Office File Converter version 2.2.0.0
    Copyright (c) 2010 Microsoft Corporation.  All rights reserved.

    Automatically converts Office documents to 2010 Microsoft Office system f
    mat based on settings in the OFC.INI control file.

    Converting files from folder E:\ExcelConv\Src
    Converting: E:\ExcelConv\Src\ie_data.xls
    Writing converted file to: E:\ExcelConv\Src\ie_data.xlsx
    Error: E:\ExcelConv\Src\ie_data.xls failed to convert
    Start:  2016-08-05 20:11:13
    End:    2016-08-05 20:11:13
    Total time used to convert files (sec): 0
    Total number of files processed: 1
    Total number of files converted: 0
    Conversion Complete.
  3. Office Binary(doc, xls, ppt) Translator
    b2xtranslator開源專案出自微軟RD,執行時只需一行指令:xls2x.exe Blah.xls就可搞定,比ofc.exe輕巧好用。 但不幸地,雖然很快轉換出xlsx,但檔案格式有錯,無法用Excel開啟。
  4. EXCELCNV.exe
    在Stackoverflow有人提到一件神祕工具-excelcnv.exe,指令如下:
    "x:\Program Files (x86)\Microsoft Office\Office15\excelcnv.exe" -oice test.xls test.xlsx
    但實測也是沒成功,執行還會導致Excel進入安全模式。
  5. 第三方元件
    有不少好用的商業元件,例如:Spire.XLS for .NETExcel Jetcell .NET,評估都很輕巧好用,但目前使用NPOI、ClosedXML已能滿足大部分需求,只為xls、xlsx轉換採購元件,報酬率偏低。

查完一輪無所獲,突發奇想:NPOI同時支援xls及xlsx,那可不可能用NPOI開啟xls再轉存xlsx呢?答案是:可能但不容易,NPOI開啟xls要用HSSFWorkbook物件,xlsx則是XSSFWorkbook,若要轉換必須開兩個物件,再一格一格螞蟻搬家。參考

無功而返。結論:面對C#讀取xls別想太多,還是乖乖用NPOI吧!

如何使用Visual Studio Code偵錯Node.js?

$
0
0

小木頭去上電腦課,一回家,想當然爾程式魔人老爸立即展開偵訊:學什麼語言?用什麼開發工具?做了什麼練習?小子對程式細節一知半解兼忘性破表,回答得語焉不詳:用一個S開頭可以編文字的軟體寫程式,開一個黑黑的視窗跑程式看結果… XD 問不出所以然。

所幸,憑著一張照片,扶耳磨絲還是解開了所有謎團:

有var、console.log、function,語言應是JavaScript,出現require跟sget,所以平台是node.js,黑黑視窗自然指的是「Node.js command prompt」吧!至於編輯器則是Sublime Text…

等等,既然是Node.js,就該用Visual Studio Code呀!不但有指令語法提示,按F5就可以測試,能設定中斷偵錯,還能即時檢查跟修改變數,很棒吧?

且讓為父的露兩手給你瞧瞧,下載裝好VSCode,這才發現忽略了一項關鍵,我根本不會用VSCode偵錯Node.js啊啊啊啊,示範個屁?

這就是本篇文章的由來,就來看看該怎麼用VSCode偵錯Node.js程式吧!

首先下載安裝Visual Studio Code,接著在裝有Node.js程式的資料夾上按右鍵開啟「Open with Code」:

VSCode開啟後會看到檔案總管,js檔已被自動開啟。

既然是Visual Studio,Intellisense當然一定要有。

接著來看看怎麼用VSCode偵錯Node.js程式。點選最左側的偵錯(禁行蟲蟲)圖示(1)會切換到偵錯視窗,偵錯鈕(2)右側顯示目前沒有組態,別擔心,按下偵錯鈕就對了。

因為沒有組態,依VSCode跳出提示,選取環境清單選取「Node.js」:

點選Node.js後VSCode會自動建立launch.json範本,預設有「啟動」「附加」「Attach to process」三種組態,要做到如Visual Studio按F5開始測試,請修改"啟動"組態中的program屬性,改為要測試的js檔。

設好組態再按一次偵錯鈕,VSCode就會呼叫Node.js執行JS程式,餘下的操作對有Visual Studio或瀏覽器F12偵錯經驗的大家應不是問題,VSCode支援設定中斷點(1),可以查看變數(2)、新増監看算式(3)、查詢呼叫堆疊(Callstack)(4),當然也可以即時執行指令(5)。

另外,身為Visual Studio家族,Goto Definition、Fill All Reference、Rename、重排程式碼… 等經典功能一定不能少,在編輯區按右鍵可由選單查到快速鍵。

補充:如果要測試互動輸入,組態檔有個externalConsole要設為true,不然遇到sget等函式會等不到輸入發生逾時錯誤。

這下算是學會如何用VSCode偵錯Node.js。小木頭來,我介紹你一個好用的程式開發工具,它叫Visual Studio Code…

【延伸閱讀】

實作Equals()與==、!=運算子注意事項一則

$
0
0

在C#自訂物件型別,基於Referece Type特性,只有兩個變數指向同一物件,==或Equals()才會傳回true(如果對Reference Type跟Value Type間的差異感到模糊,可以來個小測驗自虐釐清一番),而這常不待我們的期待。以股票代號物件為例,假設有個Ticker物件,將股票代號分為Symbol(ex: 2330)與Market(ex: TW)兩部分,另外有FullSymbol傳回2330.TW:

publicclass Ticker
{
publicstring Symbol { get; set; }
publicstring Market { get; set; }
 
public Ticker(string symbol, string market)
    {
        Symbol = symbol;
        Market = market;
    }
 
public Ticker(string fullsymbol)
    {
        var p = fullsymbol.Split('.');
if (p.Length != 2) thrownew ArgumentException();
        Symbol = p[0];
        Market = p[1];
    }
 
publicstring FullSymbol
    {
        get
        {
return Symbol + "." + Market;
        }
    }
}

測試程式中,t1,t2的內容均為2330.TW,t3則指向t1,進行Equals()及==比對:

staticvoid Main(string[] args)
    {
        var t1 = new Ticker("2330", "TW");
        var t2 = new Ticker("2330.TW");
        var t3 = t1;
 
        Console.WriteLine("Equals Test: {0}", t1.Equals(t2));
        Console.WriteLine("== Test: {0}", t1 == t2);
        Console.WriteLine("== Test(Same Object): {0}", t1 == t3);
        Console.Read();
    }

結果t1.Equals(t2)與t1 == t2都傳回false,只有t1 == t3傳回true:

Equals Test: False
== Test: False
== Test(Same Object): True

依據MSDN文章教學,我們可以覆寫Equals()、==、!=運算子自訂Ticker比較規則,判定Symbol與Market都一致就相等:

publicclass Ticker
{
publicstring Symbol { get; set; }
publicstring Market { get; set; }
 
public Ticker(string symbol, string market)
    {
        Symbol = symbol;
        Market = market;
    }
 
public Ticker(string fullsymbol)
    {
        var p = fullsymbol.Split('.');
if (p.Length != 2) thrownew ArgumentException();
        Symbol = p[0];
        Market = p[1];
    }
 
publicstring FullSymbol
    {
        get
        {
return Symbol + "." + Market;
        }
    }
 
//REF: https://msdn.microsoft.com/en-us/library/ms173147(v=vs.90).aspx
publicoverridebool Equals(System.Object obj)
    {
// If parameter is null return false.
if (obj == null) returnfalse;
// If parameter cannot be cast to Point return false.
        Ticker p = obj as Ticker;
if ((System.Object)p == null) returnfalse;
// Return true if the fields match:
return FullSymbol == p.FullSymbol;
    }
 
publicbool Equals(Ticker p)
    {
// If parameter is null return false:
if ((object)p == null) returnfalse;
 
 
// Return true if the fields match:
return FullSymbol == p.FullSymbol;
    }
 
publicoverrideint GetHashCode()
    {
return FullSymbol.GetHashCode();
    }
 
publicstaticbooloperator ==(Ticker a, Ticker b)
    {
// If both are null, or both are same instance, return true.
if (System.Object.ReferenceEquals(a, b)) returntrue;
// If one is null, but not both, return false.
if (((object)a == null) || ((object)b == null)) returnfalse;
// Return true if the fields match:
return a.FullSymbol == b.FullSymbol;
    }
 
publicstaticbooloperator !=(Ticker a, Ticker b)
    {
return !(a == b);
    }
}

重新測試,Equals()與==比對結果會依Symbol與Market是否相同決定,符合我們的期望。

staticvoid Main(string[] args)
    {
        var t1 = new Ticker("2330", "TW");
        var t2 = new Ticker("2330.TW");
        var t3 = new Ticker("1234", "TW");
 
        Console.WriteLine("Equals Test: {0}", t1.Equals(t2));
        Console.WriteLine("== Test: {0}", t1 == t2);
        Console.WriteLine("!Equals Test: {0}", !t1.Equals(t3));
        Console.WriteLine("!= Test: {0}", t1 != t3);
        Console.Read();
    }

測試結果:

Equals Test: True
== Test: True
!Equals Test: True
!= Test: True

講完了?且慢!以上範例埋藏了一個錯誤。

同事轉來ReSharper的警告:Non-readonly fields referenced in GetHashCode(),GetHashCode的計算來源必須保證不會變動,而使用readonly欄位是最直接有效的做法。而我這才注意,MSDNTwoDPoint範例,其中的x, y就是readonly,代表它們只能在建構時指定,事後不得變更。而我原本的寫法使用FullSymbol.GetHashCode(),一旦Symbol或Market變動,GetHashCode()的結果就會不同。

Eric Lippert有篇GetHashCode須知,節錄摘要相關說明下:

Rule: 相等的項目,其Hash Code必定也相同

如果兩個物件相等,其Hash Code必定相等;反之,若兩物件Hash Code不相等,其Equals()必為false。
但依邏輯學,若兩個物件的Hash Code相等,不代表物件相等。(Hash Code只有40億種變化,存在不同物件擁有Hash Code相同的機率。)

Guideline: GetHashCode傳回的整數值永遠不可改變

理想上GetHashCode應由不會異動的欄位計算而得,在物件存在的生命週期不得改變。但這只是理想,真實的規則是:至少要做到當有其他資料結構(註:例如Dictionary<T, T>,Hashtable)依賴物件的Hash Code運作時,GetHashCode()的傳回結果絕不可變動。

想像一下,若物件被放在雜湊資料結構,GetHashCode()結果卻發生改變,很明顯Contains()查詢就會壞掉。物件放進去時依Hash Code放進位置#5,修改物件Hash Code變成47,Contains()該物件時去找第#47位置,啥都沒有。

除此之外,許多LINQ運算也依賴GetHashCode()運行,一旦允許它變來變去,產生的靈異現象足以讓你鬼打牆到想改行。

洗心革面改寫程式,將Symbol及Market屬性改為唯讀,另外宣告修改readonly版欄位symbol及market,透過建構式給值,GetHashCode則改由兩個readonly欄位取值,如此才能杜絕Symbol/Market事後被修改GetHashCode()結果異動的風險:

publicclass Ticker
{
readonlystring symbol;
readonlystring market;
 
publicstring Symbol { get { return symbol; } }
publicstring Market { get { return market; } }
 
public Ticker(string symbol, string market)
    {
this.symbol = symbol;
this.market = market;
    }
 
public Ticker(string fullsymbol)
    {
        var p = fullsymbol.Split('.');
if (p.Length != 2) thrownew ArgumentException();
this.symbol = p[0];
this.market = p[1];
    }
 
//...餘略...        
 
publicoverrideint GetHashCode()
    {
return symbol.GetHashCode() ^ market.GetHashCode();
    }
 
}

大家在自訂GetHashCode()時,請留意此一原則。

Json.NET反序列化之建構式議題

$
0
0

分享處理JSON反序列化轉回物件的建構式相關問題。

就拿早先文章提到的Ticker類別當例子:

publicclass Ticker
{
readonlystring symbol;
readonlystring market;
publicstring Symbol { get { return symbol; } }
publicstring Market { get { return market; } }
publicstring FullSymbol
    {
        get { return Symbol + "." + Market; }
    }
 
public Ticker(string symbol, string market)
    {
this.symbol = symbol;
this.market = market;
    }
public Ticker(string fullsymbol)
    {
        var p = fullsymbol.Split('.');
if (p.Length != 2) thrownew ArgumentException();
this.symbol = p[0];
this.market = p[1];
    }
}

我們建立一個2330.TW Ticker,以Json.NET序列化成JSON字串,再反序列化回Ticker物件:

staticvoid Main(string[] args)
        {
            var t1 = new Ticker("2330", "TW");
            var json = JsonConvert.SerializeObject(t1);
            Console.WriteLine("JSON={0}", json);
try {
                var t2 = JsonConvert.DeserializeObject<Ticker>(json);
                Console.WriteLine("Restored={0}", t2.FullSymbol);
            }
catch (Exception ex)
            {
                Console.WriteLine("Error: {0}", ex.Message);
            }
 
            Console.Read();
        }

觸礁了~

JSON={"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}
Error: Unable to find a constructor to use for type OpTest.Ticker.
A class should either have a default constructor, one constructor with arguments or a
constructor marked with the JsonConstructor attribute. Path 'Symbol', line 1, position 10.

由於Ticker有兩個建構式,Json.NET在反序化時無法決定該用哪一個。

當物件有預設建構式(不傳任何參數),Json.NET會先以預設建構式建立物件,再一一設定屬性值。若遇到屬性值只能由物件內部指定(例如:public string Prop { get; private set; })或像Ticker使用readonly欄位,屬性只能透過建構式設定的狀況,類別不一定有預設建構式可用。

Json.NET很聰明,即使是帶參數的建構式,也會試著將JSON裡的屬性做為參數傳。例如:當JSON為{"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}而建構式為public Ticker(string symbol, string market),Json.NET會偵測參數名稱symbol、market,在JSON屬性中尋找相同名稱的屬性值(忽略大小寫),找不到則填入參數型別預設值,一樣可以建構物件。

在Ticker案例問題則出在有兩個建構式,Json.NET無法判斷要用哪一個:
public Ticker(string symbol, string market)
public Ticker(string fullSymbol)

所幸,威力強大如Json.NET,當然已料想到這類情境,提供了JsonConstructorAttribute,我們只需在其中一個建構式加上[JsonConstructor]即可解決問題:

        [JsonConstructor]
public Ticker(string symbol, string market)
        {
this.symbol = symbol;
this.market = market;
        }

最後,來對程式進行優化,順便展現Json.NET的彈性。

大家有沒有發現Ticker物件轉出的JSON字串包含Symbol、Market、FullSymbol,資料重複性太高?
{"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}

嚴格來說,只要留FullSymbol就好,用JsonIgnoreAttribute排除Symbol與Market。另外,FullSymbol有點囉嗦,
我們用JsonProperty("Tick")幫它改個簡短的名字:

publicclass Ticker
{
readonlystring symbol;
readonlystring market;
 
    [JsonIgnore]
publicstring Symbol { get { return symbol; } }
    [JsonIgnore]
publicstring Market { get { return market; } }
public Ticker(string symbol, string market)
    {
this.symbol = symbol;
this.market = market;
    }
    [JsonConstructor]
public Ticker(string fullsymbol)
    {
        var p = fullsymbol.Split('.');
if (p.Length != 2) thrownew ArgumentException();
this.symbol = p[0];
this.market = p[1];
    }
    [JsonProperty("Tick")]
publicstring FullSymbol
    {
        get
        {
return Symbol + "." + Market;
        }
    }
}

修改後的JSON樣式及測試結果如下:

JSON={"Tick":"2330.TW"}
Restored=2330.TW

JSON的長度由54個字元

{"Symbol":"2330","Market":"TW","FullSymbol":"2330.TW"}

縮短成18個字元

{"Tick":"2330.TW"}

體積縮小到1/3,這招在資料筆數量多或對傳輸量斤斤計較的場合很管用,提供大家參考。

感想:Json.NET並不是檯面上效能最好的JSON程式庫,但功能完整性及成熟度實在讓人無話可說,還是老話一句,Json.NET真該納入.NET核心的。

好了,又到了呼口號時間:

Json.NET好威呀!

修改csproj動態切換編譯程序-以DocFx為例

$
0
0

針對一些共用工具程式庫,我習慣在專案加入docfx.msbuild,每次編譯就同步產出API文件,讓文件永遠與最新版程式同步,十分方便。

不過開發久了便覺得每次編譯都重新產生文件會拖累效率,不是個好主意。以手邊的一個程式庫專案為例,沒加上DocFx前大約一秒內就能編譯完成,DocFx文件製作較耗時,動輒要耗用5-6秒,編譯時間整整拖長五倍以上,對性急如火人生苦短的中年程序員來說,彷彿感受到寶貴的職業生涯正在平白流逝,眼看累積3000安的希望愈來愈渺茫,很是煎熬。

以下是一個實例,DocFx編譯部分就花了5.821秒:

2>  Verbose: [Build Document.Apply Templates]Resource "partials/namespaceSubtitle.tmpl.partial" is found from "embedded resource docfx.Template.default.zip"
2>  Verbose: [Build Document.Apply Templates]Transformed model "X:\TFS\src\MyWebApi.Client\obj\api\MyWebApi.Models.yml" to "_site\api/MyWebApi.Models.html".
2>  Info: [Build Document.Apply Templates]Manifest file saved to X:\TFS\src\MyWebApi.Client\_site\manifest.json.
2>  Info: [Build Document]Building 85 file(s) completed.
2>  Info: Completed building documents in 3927.7096 milliseconds.
2>  Verbose: Disposing processor ConceptualDocumentProcessor ...
2>  Verbose: Disposing build step BuildConceptualDocument ...
2>  Verbose: Disposing build step CountWord ...
2>  Verbose: Disposing processor ManagedReferenceDocumentProcessor ...
2>  Verbose: Disposing build step ApplyOverwriteDocumentForMref ...
2>  Verbose: Disposing build step BuildManagedReferenceDocument ...
2>  Verbose: Disposing build step FillReferenceInformation ...
2>  Verbose: Disposing processor ResourceDocumentProcessor ...
2>  Verbose: Disposing processor RestApiDocumentProcessor ...
2>  Verbose: Disposing build step ApplyOverwriteDocumentForRestApi ...
2>  Verbose: Disposing build step BuildRestApiDocument ...
2>  Verbose: Disposing processor TocDocumentProcessor ...
2>  Verbose: Disposing build step BuildTocDocument ...
2>  Info: [Apply Theme]Theme is applied.
2>  Info: Completed executing in 5821.8844 milliseconds.
2> 
2> 
2>  Build succeeded.
2>      0 Warning(s)
2>      0 Error(s)
3>------ Build started: Project: ITForms, Configuration: Debug Any CPU ------
3>  ITForms -> X:\TFS\src\FORM\ITForms\bin\ITForms.dll
========== Build: 3 succeeded, 0 failed, 3 up-to-date, 0 skipped ==========

然而,真有必要每次編譯都重製API文件嗎?事實不然,理論上API文件只有在型別、方法介面變動時才需要更新。開發階段有很高比例的編譯動作是為了修Bug、調整寫法,此時更新API文件純屬空耗資源,平白浪費時間。

為拯救中年程序員所剩無幾的青春,我開始研究如何在專案加個開關,必要時再產生文件,平時則略過DocFx程序以縮短編譯時間。憑藉先前研究TFS Build Serivce時對MSBuild與.csproj結構的粗淺了解,我在csproj裡找到以下這段:

<ImportProject="..\packages\docfx.msbuild.1.4.2\build\docfx.msbuild.targets"
Condition="Exists('..\packages\docfx.msbuild.1.4.2\build\docfx.msbuild.targets')"/>

想必這就是DocFx編譯程序,上面已設定Condition條件,docfx.msbuild.targets檔案存在才執行。修改Condition條件,我加上$(DefineConstants.Contains('DOCFX')),指定當定義DOCFX常數時才啟用DocFx編譯。另外,將DocFx文件複製至指定位置的動作則寫成Builds後執行的Target,一定限定遇到DOCFX常數才執行。

<ImportProject="..\packages\docfx.msbuild.1.4.2\build\docfx.msbuild.targets"
Condition="$(DefineConstants.Contains('DOCFX')) AND 
 Exists('..\packages\docfx.msbuild.1.4.2\build\docfx.msbuild.targets')"/>
<TargetName="Copy DocFx Files"Condition="'$(BuildingInsideVisualStudio)' == ''"
AfterTargets="Build">
<Exec
Command="xcopy /Y /S &quot;$(ProjectDir)_site&quot; &quot;$(TargetDir)..\..\..\WebApi\Docs&quot;"
Condition="$(DefineConstants.Contains('DOCFX'))">
</Exec>
</Target>

至於DOCFX常數要如何設定?如下圖Condiional compilation symbols處,平時不加DOCFX以求快速編譯,當API介面有異動時再加上DOCFX產生文件。

歷經這番調整,算是兼顧效能與功能,編譯專案也恢復往日的速度,不再陷入「花1秒改程式,等10秒看結果」的焦慮,好多了!

分散式交易問題排除經驗再一則與MSDTC快速ASPX測試法

$
0
0

以為自己MSDTC的處理經驗已夠豐富,不料今天又有新的心得,筆記之。

某台新裝測試主機,多支涉及分散式交易程式冒出「The transaction manager has disabled its support for remote/network transactions.」錯誤,老問題一枚,推測是忘了啟用Network DTC Access。檢查果真漏了啟動選項,啟動後,其中一個ASP.NET網站的分散式交易就正常,但另一個ASP.NET網站下的ASP(對,是ASP不是ASPX,滄海桑田屹立十餘年的阿公級ASP)卻依然噴出系統不支援分散式交易的錯誤訊息:

難道這台機器上只有ASPX才支援分散式交易,ASP不行?不合理!

為了簡化測試及重現問題,把以前寫過的DTC驗證測試改成ASPX程式碼內嵌版分別丟進兩個ASP.NET網站測試:(參考:小密技-在IIS主機現場撰寫測試ASPX偵錯

<%@Page Language="C#"%>
<%@Import Namespace="System.Data" %>
<%@Import Namespace="System.Data.SqlClient" %>
<%@Import Namespace="System.Transactions" %>
<scriptrunat="server">
privatevoid querySqlServer()
{
string cnStr = "Data Source=server;User Id=user;Password=pwd;";
    cnStr += "Application Name=" + Guid.NewGuid().ToString();
using (SqlConnection cn = new SqlConnection(cnStr))
    {
        SqlCommand cmd = new SqlCommand("SELECT getdate() as D", cn);
        cn.Open();
        SqlDataReader dr = cmd.ExecuteReader();
        dr.Read();
        Response.Write("<li>" + dr["D"] + "</li>");
        cn.Close();
    }
}
 
void Page_Load(object sender, EventArgs e)
{
using (TransactionScope tx=new TransactionScope())
   {  
    Response.Write("<ul>");
    querySqlServer();
    querySqlServer();
    Response.Write("<li>" + Transaction.Current.TransactionInformation.LocalIdentifier + "</li>");
    Response.Write("<li>" + Transaction.Current.TransactionInformation.DistributedIdentifier + "</li>");
    Response.Write("</ul>");
    tx.Complete();
   }
}
</script>

執行結果讓人意外,同一支DTCTest.aspx放進ASPX網站執行OK(看到兩組GUID值),丟到ASP所在網站就出現The transaction manager has disabled its support for remote/network transactions.錯誤。同一支程式在同一機器的兩個Web Application傳回不同結果十分吊詭。優先想到是Application Pool設定不同所致,而二者最大差異在於ASP網站被設成Classic模式,而ASP.NET用的則是Integrated,雖然不覺得是關鍵,但總得試試才知。

將ASP網站的AppPool改成Integrated,但因不相容網站壞掉無法測試,只能再改回Classic,無意間再跑一次DTCTest.aspx,發現分散式交易已正常~

事後推敲,應是Classic改Integrated的過程重啟了AppPool,分散式交易支援才生效,但為什麼ASPX網站不需要重啟就能生效仍待研究。總之,為此修訂設定MSDTC之SOP,納入「設定後要IISRESET」以避免類似狀況再次發生。

跨解決方案引用專案的潛在NuGet路徑問題

$
0
0

案情說明:

     

我有個共用元件LibB,平時放在SlnB.sln這個解決方案開發。之後開發解決方案SlnA.sln需要用到LibB,原本直接引用LibB.dll,因LibB不夠成熟,時常開發到一半要加功能或修Bug。為求效率,我就把LibB.csproj也納入SlnA.sln,方便直接切專案改Code,改完重新編譯馬上測試。LibB加入SlnA後,修改過程我還用NuGet多裝了Autofac程式套件,一切進行順利,直到同事也加入開發…

同事由TFS取回SlnA與SlnB,重新編譯SlnB觸發NuGet還原機制(參考),理應自動下載補齊所有NuGet程式套件,但卻爆出LibB找不到Autofac.dll錯誤。

立即啟動NuGet參照問題SOP:打開LibB.csproj確認Reference HintPath,馬上發現異常。

由於LibB在加入SlnA後才加入Autofac,如以下所示,Autofac的HinetPath指向..\..\SlnA\packages\Autofac…(X:\src\SlnA\packages),而非以..\packages\Autofac...指向所屬解決方案目錄的packages(X:\src\SlnB\packages)

<ItemGroup>
<ReferenceInclude="Autofac, Version=4.0.0.0, Culture=neutral, 
PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
<HintPath>..\..\SlnA\packages\Autofac.4.0.0\lib\net451\Autofac.dll</HintPath>
<Private>True</Private>
</Reference>
<ReferenceInclude="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, 
PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>

換言之,當專案掛在哪個解決方案下,新増NuGet套件的位置就會指向當時解決方案的packages目錄。不管裝在哪個解決方案目錄,只要檔案俱在就相安無事,一旦遇到檔案遺失重新取回,或由另一台機器從版控抓回開發時,就可能發生問題。

在本案例中,開啟編譯SlnB時,NuGet套件將還原在SlnB\packages下,但LibB.csproj參照的來源卻是SlnA\packages,SlnA雖已下載但未編譯,NuGet套件還原來不及啟動,SlnA\pakcages目錄不存在,轟~

知道原因一切好辦,我選擇手動將LibB.csproj中的HintPath參照統一改成"..\packages\…"再重新編譯,問題就排除了。但依此經驗,未來如遇跨解決方案引用專案,需留意新増NuGet套件的HintPath路徑指向當下解決方案的特性及日後可能的副作用。一個簡易對策是「回到原解決方案再安裝NuGet套件」,應可減少類似問題發生。


ASP.NET MVC整合RichText編輯器範例與注意事項

$
0
0

最近的ASP.NET MVC專案用到了RichText編輯器,允許使用者編輯包含不同字型、大小、粗細、顏色的格式化文字,其中有些需注意細節,整理筆記備忘。

網頁版RichText編譯器的選擇不少,本文以KendoEditor為例,結果則以PostBack方式回傳。即使換用其他編輯器或改以AJAX回傳,ASP.NET MVC整合重點大同小異。

範例的MVC網站共有Index及Result兩個View,Index為編輯器頁面,Result則用來顯示結果。Controller除了Index及Result兩個Action,再增加一個Sumbit Action,負責接受前端送回內容,模擬將結果寫入DB(為求簡化,以保存在記憶體替代)供Result View讀取顯示,接著導向Result View顯示編輯結果。

HomeController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace Mvc.Controllers
{
publicclass HomeController : Controller
    {
staticstring _content = string.Empty;
void SaveToDb(string content)
        {
//模擬寫入DB
            _content = content;
        }
string ReadFromDb()
        {
//模擬由DB讀取
return _content;
        }
 
 
public ActionResult Index()
        {
return View();
        }
 
        [HttpPost]
        [ValidateInput(false)]
public ActionResult Submit(string content)
        {
            SaveToDb(content);
return RedirectToAction("Result");
        }
 
public ActionResult Result()
        {
            ViewBag.Content = ReadFromDb();
return View();
        }
    }
}

Index.cshtml已盡量簡化,網頁只有一個KendoEditor及一顆送出鈕,送出前透過JavaScript取出編輯結果(HTML)存入<input type="hidden" name="content" />,傳送給Submit Action接收:

 
@{
    Layout = null;
}
 
<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Kendo Editor Test</title>
<linkrel="stylesheet"
href="//kendo.cdn.telerik.com/2016.2.714/styles/kendo.common.min.css"/>
<linkrel="stylesheet"
href="//kendo.cdn.telerik.com/2016.2.714/styles/kendo.default.min.css"/>
<scriptsrc="//kendo.cdn.telerik.com/2016.2.714/js/jquery.min.js"></script>
<script src="//kendo.cdn.telerik.com/2016.2.714/js/kendo.all.min.js"></script>
</head>
<body>
<div>
@using (Html.BeginForm("Submit", "Home"))
        {
<textarea id="editor" style="width: 480px; height: 200px;">
黑暗執行緒
</textarea>
<input type="hidden" id="content" name="content" />
<button id="submit" type="submit">Submit</button>
        }
</div>
 
<script>
        $("#editor").kendoEditor({
            tools: [
"formatting",
"bold",
"italic",
"underline",
"strikethrough",
"foreColor",
"backColor"
            ]
        });
var editor = $("#editor").data("kendoEditor");
        $("#submit").click(function () {
            $("#content").val(editor.value());
        });
</script>
</body>
</html>

Result.cshtml也很單純,在Server端將HTML內容存入ViewBag.Content,View裡以@ViewBag.Content顯示的結果經過HtmlEncode處理(<變成&lt;)可呈現HTML原始碼,@Html.Raw(ViewBag.Content)則將HTML內容變成網頁一部分,可呈現HTML裡<h1>、<span style="color:#444">等樣式效果。注意:Html.Raw()允許使用者輸入內容成為網頁HTML語法的一部分,跟SQL Injection漏洞原理相仿,存在被注入惡意程式碼的風險,使用時需嚴加防範攻擊!這部分後面再說明。

 
@{
    Layout = null;
}
 
<!DOCTYPEhtml>
 
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>結果顯示</title>
<style>fieldset { width: 400px; height: 120px; }</style>
</head>
<body>
<fieldset>
<legend>輸入內容</legend>
<div>@ViewBag.Content</div>
</fieldset>
<fieldset>
<legend>HTML顯示結果</legend>
<div>@Html.Raw(ViewBag.Content)</div>
</fieldset>
</body>
</html>

就這樣,一個提供使用者編輯格式化文字內容的網頁介面就完成了。

接下來,來談談幾個需要注意的地方。

第一,Submit Action宣告為[HttpPost],不允許以GET方式執行。原因:永遠不要使用GET方式接收指令進行資料更新!

第二,在ActionResult Submit(string content)上有個[ValidateInput(false)],目的在關閉Request內容檢核。基於安全考量,ASP.NET MVC預設會攔截包含XML標籤的Request內容,避免有心人士透過Action注入XSS攻擊程式。但在RichText編輯情境,content包含HTML是正常的,若不設定[ValidateInput(false)]停用檢核機制,送出資料時會出現錯誤:

具有潛在危險Request.Form的值已從用戶端(content="<h2><span style="col…")偵測到。

關閉ValidateInput代表我們預期並接受content參數包含HTML語法,但於此同時也開始要承擔「content內容可能包藏XSS攻擊」風險。等等,KendoEditor並不容許輸入<script>、<iframe>,使用者應該沒法搞怪吧?錯!只要資料來自前端由使用者提供,就處處隱藏殺機,例如以下XSS注入示範:

不需用特殊道具,瀏覽器開啟F12跑一行指令,即可篡改傳送內容加入惡意程式碼,若Result View是公眾瀏覽的頁面,就可能被當成發動攻擊的跳板。

第三點,要防止使用者輸入HTML夾帶惡意程式,最有效的方法是使用Sanitizer工具進行過濾,只保留白名單列舉的HTML標籤,排除可能夾帶惡意內容的管道。至於過濾工具,過去大家蠻常用的AntiXSS Library Sanitizer,處於3.x版不夠安全,4.x版把不該殺的也殺光光的尷尬處境(4.x版被一顆星評價洗版),已不再是好選擇。重新評估,我選擇較活躍的開源專案-HtmlSanitizer

可使用NuGet安裝:

裝妥後在Submit()加上content = new HtmlSanitizer().Sanitize(content),即可過濾content可能有害的內容,前述示範惡意插入的JavaScript會整段被移除。

[HttpPost]
[ValidateInput(false)]
public ActionResult Submit(string content)
{
    content = new HtmlSanitizer().Sanitize(content);
    SaveToDb(content);
return RedirectToAction("Result");
}

重新整理重點:

  • Razor語法插入後端內容時預設會經過HtmlEncode,基本上能有效防止XSS攻擊。但RichText在呈現時必須原始呈現,需使用@Html.Raw()嵌入頁面。使用Html.Raw()代表使用者輸入內容有可能成為網頁HTML一部分,務必從嚴檢核,防範被插入惡意程式。
  • 接收資料進行更動作業的Action宜加上[HttpPost]降低被攻擊機率。
  • 接收HTML資料的Action需加上[ValidateInput(false)],避免資料傳送被封鎖。
  • 關閉ValidateInput後,防範攻擊就變成我們的責任,HTML內容進入系統前應使用Sanitizer濾掉可能有害部分。
    注意:所有可能以Html.Raw()內嵌或直接成為網頁HTML一部分的輸入參數都要加以處理。

[2016-08-19更新]

關閉ValidateInput後整個Action的所有參數都允許傳入HTML,如要進一步限定只開放某個參數接受HTML,可使用AllowHTML Attribute更安全,可一次避免其他參數被植入XSS攻擊的風險,感謝demo補充。

使用Visual Studio編譯及偵錯.NET Core專案

$
0
0

年老力衰,熱血只能花在刀口上,在技術領域嚐鮮當先鋒少不了要走冤枉路,有時更會先鋒變先烈,老年人歲月寶貴,嗯湯呀嗯湯,也因此,從不覺得自己會這麼早接觸.NET Core專案… 萬萬沒想到,今天糊里糊塗地上梁山一遊,解除了「使用Visual Studio編譯與偵錯.NET Core專案」的成就。

遇上棘手的Dapper問題,想要追進原始碼一探究竟。從Github下載了Dapper專案,用Visual Studio 2015開啟Dapper.sln,看到Solution Explorer畫面當場傻眼:

除了Dapper.StrongName,所有專案都呈現「load failed」,Output視窗則出現以下訊息。

E:\dapper-dot-net-master\Dapper.Contrib\Dapper.Contrib.xproj : error  : The imported project "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\DotNet\Microsoft.DotNet.Props" was not found. Confirm that the path in the <Import> declaration is correct, and that the file exists on disk.  E:\dapper-dot-net-master\Dapper.Contrib\Dapper.Contrib.xproj

.xproj是.NET Core專案的專案檔,相當於原本的.csproj,換句話說,為求跨平台,Dapper已轉用.NET Core專案。

爬文得知,要在Visual Studio編譯.NET Core專案,必須升級到VS2015 Update 3,並安裝NET Core 1.0.0 - VS 2015 Tooling Preview 2(參考

裝好.NET Core VS 2015 Tooling,重新開啟Dapper.sln,便可順利開啟專案及編譯。

下個問題來了,我原本寫好的Console Application專案(.csproj)可以直接參照Dapper專案(.xproj)進行逐行偵錯嗎?

答案是不行!csproj與xproj編譯原理不同,即使將兩種專案加入同一個解決方案,csproj的參照來源可以選取xproj專案,Reference也會出現Dapper項目並指向net451版dapper.dll,但編譯不會過,csproj專案抱怨不認得Dapper命名空間,形同未加參照。

爬文得知,兩種體系的專案不能直接參照,解決方法有二:參考

  1. 將xproj專案包成NuGet Package供csproj使用
  2. 將csproj轉成xproj

經評估,我的測試程式碼不多,新開一個專案比較快,以下是我的做法:

1.新増Console Application (.NET Core)專案

專案裡有個project.json,內含編譯目標平台等設定,新建專案預設為.NET Core:

{ 
"version": "1.0.0-*", 
"buildOptions": { 
"emitEntryPoint": true
  },
 
"dependencies": { 
"Microsoft.NETCore.App": { 
"type": "platform", 
"version": "1.0.0"
    } 
  },
 
"frameworks": { 
"netcoreapp1.0": { 
"imports": "dnxcore50"
    } 
  } 
}

未修改設定前,安裝.NET 4版NuGet程式套件會出錯,例如:Oracle.ManagedDataAccess:

Errors in X:\WorkRoom\OracleTools\ConsoleApp1\ConsoleApp1.xproj
    Package Oracle.ManagedDataAccess 12.1.24160419 is not compatible with netcoreapp1.0 (.NETCoreApp,Version=v1.0). Package Oracle.ManagedDataAccess 12.1.24160419 supports: net40 (.NETFramework,Version=v4.0)
    One or more packages are incompatible with .NETCoreApp,Version=v1.0.

跟project.json完全不熟(好像也沒有熟的必要,依蒐集資訊,project.json即將消失,未來將回歸csproj),參考Dapper的project.json照方煎,修改加入"net451",專案切成.NET 4.5.1就能安裝ODP.NET了。

{ 
"version": "1.0.0-*", 
"buildOptions": { 
"emitEntryPoint": true
  }, 
"dependencies": { 
"Dapper": "1.50.2-*", 
"Oracle.ManagedDataAccess": "12.1.24160419"
  }, 
"frameworks": { 
"net451": { 
"frameworkAssemblies": { 
"System.Data": "4.0.0.0", 
"System.Xml": "4.0.0.0", 
"System.Xml.Linq": "4.0.0.0"
      } 
    } 
  } 
}
如此,Console Application成功參照Dapper專案,也順利鑽進Dapper原始碼開始逐行偵錯,.NET Core技能點數+0.5。

後記:.NET Core 1.0雖已RTM,預估還需要一段時間才會成熟穩定,規格、做法應該還會有不少異動,這篇文章所提的東西或許很快就失效,大家加減參考吧~

Garmin強度分鐘怎麼算?我的人體實驗報告

$
0
0

某天晨跑到一半,我的Fenix 3彈出目標達成放煙火動畫,項目圖示是帶三條線的馬錶,印象在「我的一天」Widget看過它,跑完切到我的一天找到圖示顯示數字62,但還是不知道這數字是什麼意思?回家再看,數字已跳到67…

好奇它的意義,用「fenix3 我的一天 錶 圖示」關鍵字爬文,在彼岸論壇找到一篇帖子,同樣的疑問,附了照片但無人回答。哈,原來不只我不知道呀~ XD 這讓我對答案更加好奇,改用英文關鍵字爬文,在Garmin論壇找到一篇討論,國外也有網友跟我有相同疑問,而這回有人回覆它好像叫做Activity Minutes,高心率時間加倍計算之類的。進一步搜尋查到官方說明,它的正確名稱叫Intensity Minutes(中文翻譯成強度分鐘),概念源自vivosmart手環,Fenix 3則是在6.52版加入此功能(參考:Fenix3軟體版本歷程與功能演進,強度分鐘功能陸續多次修正,想必邏輯不只a=b+c*2這麼簡單),並且在Garmin Connect就有專屬區塊:

點下「深入瞭解強度分鐘數」,可以看到強度分鐘的組成以及相關說明:

至於活動要到什麼強度才算,要怎麼賺進強度分鐘?Garmin沒有提供明確公式,爬文找到線索拼湊如下(有些資料來自vivosmart手環,但Fenix 3計算原理應該相似):

  1. 開啟心率偵測期間,活動強度由心率與安靜心率的比值判定;若未開心率偵測,則改由每分鐘步數推算。參考
  2. 從事中強度(Moderate)或激烈(Vigorous)活動至少10分鐘才開始計算。參考
  3. 激烈強度活動(需使用心跳帶)期間的強度分鐘加倍計算。參考
  4. Garmin有一段影片介紹,Moderate被定義成可以說話但不能唱歌,Vigorous則是只能勉強說話。依據Fenix 3的心率區間(Heart Rate Zone)定義(中文部分是我胡亂翻譯的),Moderate對應到心率區間2,Vigorous則是心率區間3:
    註:如果你對如何應用心率區間提升跑步能力有興趣,運動筆記有篇好文章
    區間%最大心率 自覺強度功能
    150–60% Relaxed, easy pace, rhythmic breathing
    步伐輕鬆,呼吸規律
    Beginning-level aerobic training, reduces stress
    初級心肺訓練,放鬆及暖身
    260–70% Comfortable pace, slightly deeper breathing, conversation possible
    舒服的配速,呼吸稍重但還能聊天
    Basic cardiovascular training, good recovery pace
    基礎心肺訓練,提高恢復能力
    370–80% Moderate pace, more difficult to hold conversation
    中度配速,難以持續交談
    Improved aerobic capacity, optimal cardiovascular training
    提升有氧能力,強化心肺功能
    480–90% Fast pace and a bit uncomfortable, breathing forceful
    高配速,輕微不適,呼吸急促
    Improved anaerobic capacity and threshold, improved speed
    提高無氧能力及乳酸閾值,增進速度
    590–100% Sprinting pace, unsustainable for long period of time, labored breathing
    衝刺配速,無法持久,呼吸困難
    Anaerobic and muscular endurance, increased power
    訓練無氧耐力、肌耐力,增加功率

依據上述法則,慢跑時將心率區間拉高到3以上可賺進兩倍強度分鐘,但我發現5K跑28分鐘拿到的強度分鐘往往超過62,而停止計時後數字還會繼續上升一陣子,Fenix 3的計算依據成謎?熬

不過好奇心驅使,我展開人體實驗一探究竟。

陸續跑了幾趟,加入戴心跳帶/不戴心跳帶,開慢跑活動計時 vs 不開活動純粹戴錶跑步,慢跑計時結束繼續跑 vs 跑完就靜止休息… 等變因,為記錄強度分鐘上升狀況,還用相機拍了近百張手戴Fenix 3錶面的特寫,自己都覺得好笑。

簡單整理實驗結果如下:

實驗1

22.6K LSD,2h25m跑完,約145分鐘(中間暫停12分鐘拍照),心率區帶3.3,結束時我拿到309強度分鐘。結束後立即拿下心跳帶原地伸展,之後每分鐘+2連跳兩次,收在313。

實驗2

配速540開慢跑活動10分鐘,心率區間3.2,停止計時當下為27強度分鐘,之後繼續維持530至6分速快跑,強度分鐘以每分鐘增加2的速率一直由27上升到61,之後改為快走,歷經兩次+2降至每分鐘+1,恢復快跑後約兩分鐘,累計速度再回到每分鐘+2,一路增至69。

實驗3

配速530開慢跑活動10分鐘,心率區帶2.8,停止計時當下得23強度分鐘,之後移除心跳帶維持6分速快跑,先每分鐘加2兩次來到27,之後變成每分鐘加1一直到36,之後改為快步走,繼續每分鐘加1跳到41。

實驗4

綁心跳帶但不開慢跑活動,以6分速跑12分鐘,計步器增加約1900步,進帳14強度分鐘,之後續續快跑,以每分鐘加2速度累計到24。

實驗5

不綁心跳帶不開慢跑活動,以約530配速跑13分鐘,計步器增加超過2500步,獲得15強度分鐘。

【結論】

由以上結果,我以問答方式彙整我的實驗心得。

  1. 如何賺進強度分鐘?
    連續慢跑或快走至少10分鐘(要不要啟動活動計時皆可),Fenix 3便會開始計算強度分鐘,10分鐘後請持續運動,只要不中斷數字就會不斷累計。
  2. 一定要戴心跳帶嗎?
    不一定,但戴心跳帶賺比較快。實驗發現只要每分鐘步數達到一定門檻(基準未知,依實驗數據只知門檻在步頻160以下),就會計入強度分鐘,但戴心跳帶才能識別出激烈度,賺進兩倍強度分鐘。
  3. 強度分鐘如何跳動?何時中止?
    觀察得知,強度分鐘會連續中等強度活動10分鐘後開始計算,之後只要繼續維持活動,將每分鐘計算一次,依強度決定+1或+2,一直累計到心率及步頻都未達中等強度門檻為止,要再啟動需再連續運動十分鐘。
  4. 心率變化會馬上反應嗎?
    依實驗2,心率降低及上升約有1-2分鐘延遲才會反應+1或+2。
  5. 已知激烈活動雙倍計算,為什麼慢跑得到的強度分鐘往往比兩倍還多幾分鐘?
    觀察發只要慢跑活動平均心率區間高於3,幾乎整段時間都會雙倍計算,而結束計時後幾乎都會再跳兩次+2(即使靜止並拿掉心跳帶,如實驗1),推測為延遲計算原則。而開始跑步前的步行與活動,結束後的緩和運動及伸展期間,只要步頻或心率達到門檻,一樣會計入強度分鐘,應可解釋每次慢跑獲得兩倍時間再多送幾分鐘。
  6. 一定要運動才能賺進強度分鐘嗎?
    不一定,依原理只需讓心跳長期維持高檔,靜止不動應該也成。故戴心跳帶坐大怒神(連坐十分鐘會死吧?)、跟女神約會(見面不到兩分鐘就被打槍… 不對,醒醒吧,你根本約不到女神)、考試作弊、劈腿偷情、作賊行竊… 應該都有效果。(大誤)
  7. 強度分鐘可以換錢嗎?還是有人拿來拼輸嬴?
    不行。沒有。
  8. 那,為什麼要花這麼大功夫研究這些,你有病嗎?
    對,我有病。

報告完畢。

註:我測試的軟體版本為7.2,不同版本計算邏輯可能略有不同。(另外,7.5版有出現「人在家中坐,數字飆上天」的Bug Report

Hacking樂無窮:修正Dapper+ODP.NET無法寫入Unicode問題

$
0
0

歷經一段時間摸索歷練,確立「新増修改用EF/ORM,查詢一律用Dapper」的最高指導原則,Dapper的簡潔、效能與彈性無可挑剔,一切看似完美,直到我膝蓋中了一箭…

無意間發現,使用Dapper+ODP.NET無法寫入Unicode字元

跟Oracle Unicode問題奮戰超過10年,以為妖孽已被降伏,用OracleDbType.NVarChar2應該就萬無一失,甚至要在CommandText中用N'…'也不是問題,萬萬沒想到Oracle Unicode問題今天又跑出來咬我屁股。

用以下範例重現問題:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Oracle.ManagedDataAccess.Client;
 
namespace OraUnicodeTest
{
publicclass Program
    {
staticstring csOra = "…略";
staticstring csSql = "…略";
 
staticvoid Main(string[] args)
        {
using (var cn = new OracleConnection(csOra))
            {
                cn.Execute("UPDATE JEFFTEST SET NAME=:NAME WHERE SEQNO=:SEQNO",
new
                    {
                        SEQNO = 1,
                        NAME = "牛牪犇" + DateTime.Now.Millisecond
                    });
            }
using (var cn = new SqlConnection(csSql))
            {
                cn.Execute("UPDATE JEFFTEST SET NAME=@NAME WHERE SEQNO=@SEQNO",
new
                    {
                        SEQNO = 1,
                        NAME = "牛牪犇" + DateTime.Now.Millisecond
                    });
            }
        }
    }
}

用標準cn.Execute("UPDATE … SET Col=:V1 WHERE …", new { V1 })寫法同時更新Oracle及SQL寫入Unicode文字,在SQL Server一切正常,在Oracle六頭牛跑了三頭!

SQL Server

Oracle

追進Dapper原始碼,Dapper為求跨資料庫,故只能依賴IDbCommand等通用介面,使用CreateParameter產生參數物件,參數型別也必須採通用的System.Data.DbType列舉。至於字串,除非以new DbString() { Value="…", IsAnsi = true }指定System.Data.DbType.AnsiString,字串一律視為System.Data.DbType.String,支援Unicode字元。

由此推論ODP.NET在處理Parameter型別DbType.String時,理應對應成絕對支援Unicode字串的OracleDbType.NVarChar2,卻誤對應成OracleDbType.VarChar2。爬文更發現,不只Dapper,一些使用DbType.String的跨平台程式庫,遇上ODP.NET時也紛紛中箭落馬,例如:NHibernate

用以下實驗驗證ODP.NET處理DbType.String參數有問題:

cn.Open();
var cmd = cn.CreateCommand();
var p = cmd.CreateParameter();
p.ParameterName = "N";
p.DbType = System.Data.DbType.String;
//p.OracleDbType = OracleDbType.Varchar2;
//p.OracleDbType = OracleDbType.NVarchar2;
p.Value = "牛牪犇" + DateTime.Now.Millisecond;
cmd.CommandText = "UPDATE JEFFTEST SET NAME=:N WHERE SEQNO=2";
cmd.Parameters.Add(p);
cmd.ExecuteNonQuery();

實測結果:OracleParameter設定DbType = DbType.String或設定OracleDbType = OracleDbType.Varchar2i時「犇」字都無法正確寫入;必須OracleDbType = OracleDbType.NVarchar2才會正常。

依照System.Data.DbType的設計,DbType.AnsiString對應OracleDbType.Varchar2,DbType.String對應OracleDbType.NVarchar2才合理, 怎麼都覺得是ODP.NET的錯。但為什麼這個錯誤沒有引發大量災情?猜想與它需要以下條件同時成立才會發生有關:

  1. Oracle資料庫未採AL32UTF8編碼
    新建立的資料庫多採UTF8編碼,VARCHAR2即可存入Unicode,因此DbType.String對應成OracleDbType.Varchar2也沒差。本次處理的Oracle環境為求與老系統相容,還在使用ZHT16MSWIN950編碼。
  2. OracleParameter參數型別未指定OracleDbType,而是指定DbType
    OracleParameter同時具備OracleDbType及DbType,都可以設定參數型別。直接使用ODP.NET時,我們多半會指定OracleDbType明確選用NVarchar2或Varchar2,只有Dapper、NHibernate這類必須跨資料庫的程式庫,才會使用與資料庫種類無關的DbType。
  3. 寫入內容剛好有ANSI/BIG5難字
    Dapper寫入NVarchar2的做法已應用在不少地方,這次碰巧寫入資料帶有BIG5難字,問題才爆出來。

這問題挺嚴重,寫Unicode變空白或亂碼誰都不能接受,但要為此放棄Dapper?研判這是ODP.NET的Bug,但感覺被困擾的人不多,Oracle不會積極處理,難道就只能束手無策嗎?

不,誰都別想惹他媽的程式老魔人,誰都別想~ (註:有人提問,補上小河馬典故

判斷是ODP.NET的Bug,但錯誤不普及,短期被修復的可能性不大。但不修正,Dapper無法正確更新Oracle NVARCHAR2,等同廢了一條腿,怎麼辦?

該是駭客登場的時候了,換上墨鏡跟黑色長大衣,打開JustDecomplie反組譯Oracle.ManagedDataAccess.Client.dll鎖定問題根源。

在DbType屬性的set段找到邏輯,當設定OracleParameter.DbType時,背後會同步修改m_oraDbType屬性,而什麼DbType要對應到什麼OracleDbType,由一個內部靜態類別,OraDb_DbTypeTable,的陣列資料:int[] dbTypeToOracleTypeMapping決定。

再追進OraDb_DbTypeTable,靜態建構式裡以Hard-Coding方式指定哪一種DbType列舉要對應成哪一種OracleDbType。先查出各列舉對整數:

(int)OracleDbType.NVarchar2 = 119
(int)OracleDbType.Varchar2 = 126
(int)System.Data.DbType.String = 16

dbTypeToOracleDbTypeMapping[16]=126,罪證確鑿!DbType.String被對應成OracleDbType.Varchar2,是造成Unicode字元無法寫入DB的元兇!

找到根源就好辦,在駭客眼裡,類別屬性欄位哪有分什麼public、internal、private,System.Relection拿出來,想怎麼讀就怎麼讀,愛怎麼改就怎麼改。

我寫了以下修正方法覆寫dbTypeToOracleDbTypeMapping將DbType.String改指向OracleDbType.NVarchar2,修正後ODP.NET + Dapper無法寫入Unicode問題就煙消雲散了。(註:FixOdpNetDbTypStringMapping請放在Glabal.asax.cs或程序、靜態類別啟動過程,整個Process執行一次即可)

staticvoid FixOdpNetDbTypeStringMapping()
{
    Assembly asm = typeof(OracleConnection).Assembly;
    Type tOraDb_DbTypeTable = asm.GetType("Oracle.ManagedDataAccess.Client.OraDb_DbTypeTable");
    var fldDbTypeMapping = tOraDb_DbTypeTable.GetField("dbTypeToOracleDbTypeMapping", 
        BindingFlags.Static | BindingFlags.NonPublic);
int[] mappings = (int[])fldDbTypeMapping.GetValue(null);
    mappings[(int)System.Data.DbType.String] = (int)OracleDbType.NVarchar2;
    fldDbTypeMapping.SetValue(null, mappings);
}

解決了一個心腹大患,Hacking樂無窮~

利用LINQ GroupBy快速分組歸類

$
0
0

分享最近學到的LINQ小技巧一則。有時我們會需求將資料物件分組擺放,方便後續查詢處理,例如:將散亂的銷售資料依客戶分群,同一客戶的所有資料變成一個List<T>。

過去面對這種問題,我慣用的做法先定義一個Dictionary<string, List<T>>,使用 foreach 逐筆抓取來源資料,從中取出鍵值(例如:客戶編號),先檢查鍵值是否已存在於Dictionary,若無則新増一筆並建立空的List<T>,確保Dictionary有該鍵值專屬List<T>,將資料放入List<T>。執行完畢得到以鍵值分類的List<T>,再進行後續處理。

foreach + Dictionary寫法用了好幾年,前幾天才忽然想到,這不就是SQL語法中的GROUP BY嗎?加上LINQ有ToDictionary, GroupBy(o => o.客戶編號).ToDictionary(o => o.Key, o => o.ToList()) 一行就搞定了呀!阿呆。

來個應景的程式範例吧!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace LinqTip
{
class Program
    {
publicenum Teams
        {
            Valor, Mystic, Instinct, Dark
        }
 
publicclass Trainer
        {
public Teams Team;
publicstring Name;
public Trainer(Teams team, string name)
            {
                Team = team; Name = name;
            }
        }
 
staticvoid Main(string[] args)
        {
//來源資料如下
            List<Trainer> trainers = new List<Trainer>()
            {
new Trainer(Teams.Valor, "Candela"),
new Trainer(Teams.Valor, "Bob"),
new Trainer(Teams.Mystic, "Blanche"),
new Trainer(Teams.Valor, "Alice"),
new Trainer(Teams.Instinct, "Spark"),
new Trainer(Teams.Mystic, "Tom"),
new Trainer(Teams.Dark, "Jeffrey")
            };
//目標:以Team分類,將同隊的訓練師集合成List<Trainer>,
//最終產出Dictionary<Teams, List<Trainer>>
 
//以前的寫法,跑迴圈加邏輯比對
            var res1 = new Dictionary<Teams, List<Trainer>>();
foreach (var t in trainers)
            {
if (!res1.ContainsKey(t.Team))
                    res1.Add(t.Team, new List<Trainer>());
                res1[t.Team].Add(t);
            }
 
//新寫法,使用LINQ GroupBy
            var res2 =
                trainers.GroupBy(o => o.Team)
                .ToDictionary(o => o.Key, o => o.ToList());
        }
    }
}

就醬,又學會一招~

不過,GroupBy().ToDictionary() 做法適用分類現有資料,若之後要陸續接收新增資料,仍可回歸 foreach + Dictionary<string, List<T>> 寫法。

[2016-08-24補充] 感謝Phoenix補充,LINQ還有更簡潔的做法:ToLookup(o > o.Teams, o => o),其產出的型別為ILookup,以Key分組的Value集合,與Dictionary最大的差異是ILookup屬唯讀性質,事後不能變更或修改集合項目。

神祕的ASP.NET bin\roslyn目錄

$
0
0

同事由TFS取回ASP.NET MVC專案,編譯後執行出現以下錯誤:

[DirectoryNotFoundException: 找不到路徑 'D:\TFS\src\web\MyForm\bin\roslyn\csc.exe' 的一部分。]
System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath) +353
System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost) +1326
System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share) +65
Microsoft.CodeDom.Providers.DotNetCompilerPlatform.Compiler.get_CompilerName() +91
Microsoft.CodeDom.Providers.DotNetCompilerPlatform.Compiler.FromFileBatch(CompilerParameters options, String[] fileNames) +656
Microsoft.CodeDom.Providers.DotNetCompilerPlatform.Compiler.CompileAssemblyFromFileBatch(CompilerParameters options, String[] fileNames) +186
System.CodeDom.Compiler.CodeDomProvider.CompileAssemblyFromFile(CompilerParameters options, String[] fileNames) +24
System.Web.Compilation.AssemblyBuilder.Compile() +950
System.Web.Compilation.BuildProvidersCompiler.PerformBuild() +10029581
System.Web.Compilation.ApplicationBuildProvider.GetGlobalAsaxBuildResult(Boolean isPrecompiledApp) +9979064
System.Web.Compilation.BuildManager.CompileGlobalAsax() +44
System.Web.Compilation.BuildManager.EnsureTopLevelFilesCompiled() +260

bin\roslyn\csc.exe?ASP.NET什麼時候冒出這玩意?印象裡又彷彿看過… 查了手上幾個ASP.NET專案的bin目錄,還真有個roslyn目錄,裡面有C#及VB的Compiler執行檔,還有一堆DLL:

追進csproj檔,有設定指向\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform、\packages\Microsoft.Net.Compilers…

<Import Project="..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\build\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.props" Condition="Exists('..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\build\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.props')" />
<Import Project="..\packages\Microsoft.Net.Compilers.1.0.0\build\Microsoft.Net.Compilers.props" Condition="Exists('..\packages\Microsoft.Net.Compilers.1.0.0\build\Microsoft.Net.Compilers.props')" />

<Reference Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
  <HintPath>..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll</HintPath>
  <Private>True</Private>
</Reference>

<Error Condition="!Exists('..\packages\Microsoft.Net.Compilers.1.0.0\build\Microsoft.Net.Compilers.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Net.Compilers.1.0.0\build\Microsoft.Net.Compilers.props'))" />
<Error Condition="!Exists('..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\build\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.1.0.0\build\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.props'))" />

由此推測這是ASP.NET透過NuGet安裝,用於執行期間動態編譯程式碼的程式套件。使用NuGet管理員,驗證ASP.NET專案被安裝了Microsoft.CodeDom.Providers.DotNetCompilerPlatform,而Microsoft.NetCompilers是其依賴的底層套件。套件說明提到,ASP.NET傳統是用CodeDOM解決執行期間動態編譯需求,新版ASP.NET已改用新世代的.NET編譯平台(Roslyn)。

Roslyn是2014推出的新一代Open Source編譯平台,Visual Studio 2015起改用Roslyn作為編譯核心,ASP.NET專案樣版也開始改用Roslyn處理MVC View、WebForm Inline Code動態編譯,這就是bin/roslyn目錄的由來。

回到最初的問題,為什麼同事從TFS取回我的ASP.NET專案編譯會缺少Roslyn套件呢?

老問題!.csproj搬過家,由Sln/Blah.csproj搬到Sln/Web/Blah.csproj,還記得之前遇過的..\packages改..\..\packages NuGet HintPath問題嗎?csproj裡Roslyn CodeDOM設定仍指向..\packages,搬家後需手動修改。

問題在修改路徑修後排除,結案。

筆記:使用Google帳號登入ASP.NET MVC網站

$
0
0

新版ASP.NET改採OWIN架構,Middleware概念讓Request/Response處理流程變得模組化,允許抽換自由組裝,身分驗證也變得極富彈性,IIS時代ASP.NET只有匿名、Basic、Windows幾種選擇,改用OWIN後,整合Google、Facebook、Twitter、Microsoft Account… 等OAuth登入服務不再是難事,還有現成元件可用,輕輕鬆鬆實現Google、FB登入。(關於OAuth,小朱有系列文章介紹,包含原理與OAuth Client實作細節)

要做到Google、FB登入,使用ASP.NET 4.5.2的MVC專案樣版是捷徑,專案本身即內建Google、Facebook、Twitter、Microsoft登入整合功能。

新増專案時,Authentication選項預設為Individual User Accounts,採用ASP.NET自家的Membership機制,且支援使用Facebook、Twitter、Google、Microsoft帳號登入。

啟用外部帳號登入需修改程式碼,開關藏在App_Start/Startup.Auth.cs中:

有四段被註解掉的程式碼,分別是MicrosoftAccount、Twitter、Facebook跟Google的身分驗證設定,移除註解填入各家要求的API Key及Secret即可啟動,之後登入畫面會多出已啟用帳號服務的登入鈕,只要API相關設定妥當,按鈕後網頁會導向Google、FB網站,待使用者完成登入並授權後再回到MVC網站,ASP.NET端取得使用者的Google、FB帳號資料,再與系統自己的使用者資料結合完成身份識別,免除管理帳號密碼的負擔,非常方便。

感覺是小菜一碟,ASP.NET端只要填兩個值就搞定,但上面提到「只要API相關設定妥當」這句話暗藏玄機。因為缺乏經驗,加上網路服務改版迅速,現況與爬文所得常有出入,摸索好一陣子我才搞定Google API,而在整合FB花的時間更是可觀… 整理攻略如下,希望能幫大家節省一點時間。(ASP.NET官網上有篇文章介紹得挺詳細,但文章寫於2015年4月,有些地方已有變化)

這篇先介紹登入Google登入。

Google API Key要到Google API Console申請,第一件事請確定資訊主頁的API清單中有「Google+ API」,若沒有請使用上方的「啟用API」鈕新増。(一時糊塗,我在此挨了悶棍)

接著到「憑證」區,建議先設定「OAuth同意畫面」,填寫要顯示在授權網頁的資訊:

下一步,「建立憑證」,類別選「OAuth用戶端ID」:

在憑證設定畫面,應用程式類型請選「網路應用程式」,名稱無關緊要隨便敲,至於「已授權的JavaScript來源」要填入網站URL(不含路徑,localhost也接受),「已授權的重新導向URL」則填網站URL加上"/signin-google":

憑證建立後,Google很貼心地顯示用戶端ID(ClientId)、用戶端密碼(ClientSecret)並有按鈕直接複製字串到剪貼簿:

將ClientId及ClientSecret複製到Startup.Auth.cs:(提醒:實務開發時應由appSetting取值,不宜寫死)

            app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
            {
                ClientId = "6847…7b6d.apps.googleusercontent.com",
                ClientSecret = "Ad1…pfZ"
            });

回到MVC網站的登入頁面,按下登入頁面的「Google」按鈕,網頁會切到Google登入及授權畫面:

按下「允許」回到MVC網站,系統已從Google取得Email資料,可直接註冊成會員。

註:測試過程學到一則經驗,「已授權的重新導向URL」在修改後似乎不會立刻生效,如在測試開發階段,重建憑證比較快。

下一集來談FB帳號整合,過程曲折許多,為了取得Email,我不但鑽了原始碼,還修改元件才過關,很硬斗!請待下回分解。


筆記:使用Facebook帳號登入ASP.NET MVC網站

$
0
0

前篇文章介紹過ASP.NET MVC 5內建Google、FB、Twitter、Microsoft Account外部帳號登入支援,只需設定API Key及API Secret即可啟用,十分方便。上回整完Google帳號,繼續處理Facebook登入。

一回生二回熟,第一步當然是到Facebook開發者網站申請API Key。在網頁按下「新増應用程式」:

類型選「網站」:

為應用程式取個名字:

填寫「顯示名稱」、「聯絡電子郵件」及「類別」:

下一步要填寫網站URL:

基本上這樣就算設定好了,「應用程式編號」跟「應用程式密碼」即程式所需的AppId及AppSecret。補充一點,Facebook API不像Google可以設多組URL,開發機、測試台、正式機URL不同需要多組AppId,可善用左側選單「建立測試應用程式」快速產生另一組AppId及AppSecret方便測試。

取得AppId及AppSecret後,在Startup.Auth.cs加入設定:(提醒:實務上appId及appSecret應由appSettings讀取,不宜寫死。)

            app.UseFacebookAuthentication(
               appId: "7360…1957",
               appSecret: "258d…8509");

實際測試,在MVC登入網頁按下Facebook按鈕,瀏覽器會導向Facebook網站,進行登入及要求授權:

待使用者同意授權後,會再導向MVC網站,此時ASP.NET已取得使用者的Facebook身分,拿到使用者的Email… 咦?並沒有!Email欄位是空的,跟Google帳號登入的情況不同。

依照ASP.NET會員管理需求,只要Facebook針對特定使用者提供唯一且固定的識別碼與會員資料連結就已夠用,Email交由使用者自由填寫不一定要與FB相同。但我有不同計劃,打算以Google、Facebook傳回Email作為權限管控依據,故還是得設法拿到Email。

只因為比一般應用情境多了一點要求,我離開寫兩行程式就搞定的高速公路,再次走上「抓Code看Code改Code」這條崎嶇蜿蜒的產業道路…(好討厭的感覺~)

使用Visual Studio偵錯發現,外部帳號登入成功後會使用AuthenticationManager.GetExternalLoginInfoAsync()方法取得使用者基本資料,DefaultUserName為使用者姓名、Email為電子郵件,另外有個Login屬性為UserLoginInfo型別,其中LoginProvider為"Facebook",另外有個ProviderKey,使用Facebook登入時,Email是空的。

而Google登入則可以抓到Email:

深入了解,得知Facebook從Graph API 2.0起改採App-Scoped User Id(中文翻成應用程式範圍編號)以強化個資保護,應用程式無法取得使用者真實User Id,改為首次登入應用程式時動態產生識別序號,該序號只對該應用程式有效,而應用程式只能取得有限的個人資訊,想取得更多資訊需經過審查。ProviderKey即是所謂的App-Scoped User Id。

依據官方文件,不需要審查可取得的資料範圍包含public_profile(姓名、連結、照片等)、email、user_friends。從CodePlex Katana專案拿到Microsoft.Owin.Security.Facebook的原始碼,發現Startup.Auth.cs UseFacebookAuthentication可以額外指定抓取email Scope:

            var opt = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions()
            {
                AppId = System.Configuration.ConfigurationManager.AppSettings["FBApiKey"],
                AppSecret = System.Configuration.ConfigurationManager.AppSettings["FBApiSecret"]
            };
            opt.Scope.Add("email");
 
            app.UseFacebookAuthentication(opt);

很不幸,加入email Scope後還是抓不到Email資料。逼得我得架設深入Microsoft.Owin.Security.Facebook內部偵錯並重現問題的測試環境(過程很繁瑣磨人,細節就不說了),費了一番手腳找出問題點。在Microsoft.Owin.Security.Facebook.FacebookAuthenticationHandler.AuthenticationCoreAsync()有一段程式呼叫 httqs://graph.facebook.com/me?access_token=… 取回App-Scoped User Id、姓名,舊版Graph API傳回結果預設即包含Email資料,新版API則需加上fields參數才會回傳Email,故程式需修改如下:(感謝小朱補充:fields參數需求是2.4版加入的規格)

歷經N個小時努力,Facebook登入終於也能抓到Email了,萬歲~

你的密碼被偷了嗎?

$
0
0

兩天前收到Dropbox的通知信,說我從2012年起就沒有變更過密碼,為了安全起見,下次登入時系統會提示進行更新。

信件與網頁強調這單純是預防性措施,帳戶並沒有被不當存取的跡象(實際登入Dropbox網站檢視存取記錄,的確也都正常),原因是Dropbox發現有一組舊的使用者登入資料 (電子郵件地址加上加密及雜湊的密碼)在2012年被偷走,Dropbox經由監測與分析相信未有帳戶遭到不當存取。不過為了安全起見還是要求所有 2012 年中之後就未曾更新的使用者,在下次登入時重設密碼。

今天看到Troy Hunt(有名的資安領域MVP)的文章,他拿到Dropbox流出的6800萬筆帳號、密碼雜湊檔,正巧包含他太太的Email帳號與密碼雜湊值。密碼由密碼管理工具(1Password)產生,是一組超過20字元的隨機字串,於是Troy做了簡單實驗,用密碼破解工具(hashcat)驗證Dropbox流出資料的雜湊碼的確是用該密碼計算出來的,換言之,檔案是真實資料無誤。

文章有提到Troy Hunt建立的資安服務網站「Have I been pawned?」,網站蒐集重大帳號密碼外流事件的資料整理成資料庫,不需註冊或登入,在網頁輸入Email或帳號名稱就可以查查自己的帳號密碼個資甚至信用卡資料是否曾被竊取或公開。

拿我的Email查了一下:

靠北,我中了!

我的Email名列本次Dropbox事件的高危險群(難怪會收到Dropbox的通知信),查詢結果顯示「Pwned on 1 breached site found no pastes.」代表帳號或Email出現在一個網站的外洩資料中,而Paste是指資料被公開散佈(例如:被貼到匿名公佈欄),而本次Dropbox的資料尚未公開流傳,但很可能已在駭客圈私下交流多年。我的因應之道是改用KeePass產生一組32字元的新密碼,並啟用兩階段驗證(加上手機簡訊驗證)。

未來再有帳號密碼外洩事件,未必每個廠商都像Dropbox一樣願意主動告知,Have I been pawned是可以馬上查證自己是否身陷險境的好地方~

【茶包射手日記】網站在IIS下無法讀取LocalDB

$
0
0

小問題一則。

為了測試Google登入整合,我將ASP.NET網站上傳Azure。經本機IIS Express測試無誤的網站,一掛到IIS下執行卻出現錯誤,在事件檢視器有以下訊息:

    • Unexpected error occurred while trying to access the LocalDB instance registry configuration. See the Windows Application event log for error details.
    • Windows API call SHGetKnownFolderPath returned error code: 5. Windows system error message is: Access is denied.
    • Cannot get a local application data path. Most probably a user profile is not loaded. If LocalDB is executed under IIS, make sure that profile loading is enabled for the current user.

ASP.NET網站使用LocalDB存放使用者註冊、角色等資料,推測是無法正確載入資料庫出錯。訊息明確與Profile有關,IIS使用Application Pool處擬帳戶,而IIS Express使用開發者登入Windows的帳戶執行,推測為導致錯誤的關鍵。所幸訊息明確,馬上查到相關說明

LocalDB需要載入User Profile才能運作,而IIS Application Pool預設不會載入,故需手動修改C:\Windows\System32\inetsrv\config\applicationHost.config,找到<system.applicationHost><applicationPools>裡該AppPool對應設定,將loadUserProfile及setProfileEnvironment設為true:

<add name=“OAuthTest“ autoStart=“true“ managedRuntimeVersion=“v4.0“ managedPipelineMode=“Integrated“>
<processModel identityType=“ApplicationPoolIdentity“ loadUserProfile=“true“setProfileEnvironment=“true“ />
</add>

註:LocalDB主打輕巧簡便及與正式SQL行為相近,但由於運作範圍僅限單一應用程式,多用於開發測試,正式及中大型應用時還是會使用正規SQL Server。關於LocalDB,保哥有一篇詳細介紹可以參考。

再談T-SQL複合字串鍵值比對-借用PARSENAME()

$
0
0

以下是我實際遇到的情境,複合鍵在某些資料表拆成兩個或三個欄位,但在某些資料表則用"."或"-"串接存成單一欄位。(不要問我為什麼搞出這種不一致的設計,誰沒有過去?)

如以下的例子,在JStock資料表的Market及Symbol欄位,在JReport則使用一個FullSymbol欄位,存成"Symbol.Market"。

問題來了,如果這兩個Table要JOIN怎麼辦?過去用過一種鳥方法:
JReport R JOIN JStock S ON R.FullSymbol = S.Symbol + '.' + S.Market格式。

這方法挺管用,但可能會有效能問題。以上面的例子,假設JStock有兩萬筆,JReport查詢結果只有四筆,為了找出這四筆對應的資料,SQL Server得把整個JStock每一筆的Market跟Symbol拿出來先相加再比對,無法善用JStock的Index加速。

先前曾介紹過用SQLXML拆字串的技巧,但XML轉換演算法太曲折且效率不佳,前幾天無意發現一個T-SQL內建函式-PARSENAME()(SQL 2008 R2起支援),原本用來將SvrName.DbName.SchemaName.ObjectName拆解成四個部分,例如以下範例:

只要複合字串的分隔符號是"."(若不是,可用REPLACE置換),就能用PARSENAME擷取指定一段落(最多能只解析四段,應能滿足絕大部分場合),不用寫自訂函數,效能又比XML轉換好。

最後補充一點,以上做法雖善用JStock Symbol、Market Index提升效能,但使用PARSENAME解析JReport FullSymbol仍會消耗效能。如果允許變動Schema以空間換取時間,可考慮為JStock建立計算型欄位(Computed Column)-FullSymbol = Symbol + '.' + Market,設為IsPersisted還能建立Index,直接使用FullSymbol = FullSymbol比對,效能可再大幅提升。

惱人的Managed ODP.NET ConfigSection問題

$
0
0

自從學會Managed ODP.NET,它馬上成為我的奧林匹克指定資料庫元件。不用額外安裝Oracle Client,管它x86還是x64,只要在主機設好TNSNAMES.ORA(我慣用的做法是用%TNS_ADMIN%環境參數提供路徑,一台主機只要設一次,部署到不同主機時不需改config),用NuGet下載安裝好一切搞定,十分方便。比起傳統ODP.NET常常糾結於x84與x64與Oracle Client版號高低,Managed ODP.NET高雅先進,用過之後就回不去了。

不過,我常遇到一個小問題,使用NuGet安裝Managed ODP.NET會自動在web.config加以下區段宣告:

<configSections>
    <section name="oracle.manageddataaccess.client" type="OracleInternal.Common.ODPMSectionHandler, Oracle.ManagedDataAccess, Version=4.121.2.0, Culture=neutral, PublicKeyToken=89b483f429c47342" />
</configSections>

在Visual Studio裡以IIS Express執行會出現以下錯誤:

Error Code       0x800700b7
設定錯誤       定義了重複的 'oracle.manageddataaccess.client' 區段
設定檔案       \\ ? \X:\TFS\src\Forms\web.config

研判machine.config已定義過oracle.manageddataaccess.client區段 ,我加上<!--把它註解掉,IIS Express即可正常執行。為了方便測試,我還習慣將同一專案路徑也掛在IIS下,好處是不需要在Visual Studio按F5/Run/Debug就能測試,還可開放其他同事預覽(IIS Express僅限localhost存取)。有趣的事發生了,網站在IIS出現以下錯誤: 

錯誤碼       0x80070032
設定錯誤       無法讀取設定區段 'oracle.manageddataaccess.client',因為它缺少區段宣告
設定檔案       \\ ? \X:\TFS\src\Forms\web.config

在IIS Express裡加oracle.manageddataaccess.client區段會出錯,拿掉區段宣告在IIS裡會出錯,你們搞得我好亂…

不過仔細一想,啊,machine.config!32與64!

IIS Express之所以抱怨區段重複宣告是因為machine.config已經宣告過,IIS抱怨缺少區段宣告則是因為machine.config沒宣告,到底machine.config有宣告還是沒有宣告?答案是一個有宣告,一個沒宣告!machine.config有分x86與x64版本,分別位於C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config 及 C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config。IIS Express一律以32位元模式執行,IIS的執行環境由AppPool設定決定,預設為64位元模式。之前為了寫文章,執行過Managed ODP.NET安裝install_odpm.bat c:\oracle x86 true,只裝了x86版,導致只有32位元版machine.config加入oracle.manageddataaccess.client區段宣告,64位元版machine.config沒有,全案宣告偵破!

找出原因一切好辦,要解決有兩個做法:

  1. 執行install_odpm.bat c:\oracle both true,讓32/64 machine.config一致。
  2. NuGet之所以修改web.config加入區段宣告,是因為它加了一段範例設定:
      <oracle.manageddataaccess.client>
        <version number="*">
          <dataSources>
            <dataSource alias="SampleDataSource" descriptor="(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCL))) " />
          </dataSources>
        </version>
      </oracle.manageddataaccess.client>
    如果不需要這段設定(像我就是用環境變數解決),將它連同一開始的configSection都移除也能避開問題。

提醒,類似的情境也會發生在部署到測試機與正式機時,需依部署對象machine.config狀況(記得區分32或64)決定是否加上configSection宣告。

Viewing all 428 articles
Browse latest View live