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

【茶包筆記】 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

【茶包射手日記】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)],避免資料傳送被封鎖。
  • HTML內容進入系統前應使用Sanitizer濾掉可能有害部分。

使用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,搬家後需手動修改。

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

Viewing all 2311 articles
Browse latest View live