2014年3月13日 星期四

JOKE


  1. 當個 龍馬精神 的 神經碼農 - 2014/06/27
  2. 所謂的會議紀錄 指的是 下一次開會就會 timeout 的文件 - 2014/04/29
  3. 曾子曰:吾日三省吾身
    客戶曰:吾日三改需求 - 2014/03/13
  4. 質量守恆 但是 職量不守恆 - 2014/01/29

2014年2月14日 星期五

AspNet Identity 玩弄手札 第二彈 Social Login Facebook 登入

One ASP.NET 表示:天下大勢,分久必合,合久必分

AspNet Identity 表示:我也不過是被 One ASP.NET 玩弄於股掌之間的一個犧牲者

老疆表示:程式設計師用一些想到的點子來做一些無聊事情,這樣瞭不瞭

第一次聽到 AspNet Identity 是 老疆 的影片

http://www.youtube.com/watch?v=G2JC4KWTqXM&list=PLSvKFCIcFiosRfhmjcSCozircig3dRRrR&index=1

(11:15開始)

老疆的演講都很有梗

而且梗都是模仿不來的

別人說都不好笑 但是聽他講卻又有種獨特的幽默

我想這是一種 Aura









這次來玩 Social Login

為啥要 Social Login

也沒啥為什麼 因為流行

客戶說 OO 的網站可以用 XX 登入 我們的網站也要以

但是 social website 滿天下

每個都自成一套 API

而且 document 絕不會有中文

所以程式設計師總是永無止境的在熟悉 API 的用法

等到好像有點懂的時候 可能 API 又改版了 ._.




Micro$oft 發現了這個現象

所以就把它包裝起來 放到 AspNet Identity 裡面

好讓它更顯強大




首先是出場率最高的 Facebook 登入

在以往要做到這項功能

可能要先征服這頁

https://developers.facebook.com/docs/facebook-login/login-flow-for-web/



有個名詞兒叫 OAuth

OAuth(開放授權)是一個開放標準,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。

http://zh.wikipedia.org/wiki/OAuth




Facebook 登入 就是一種 OAuth

那不重要 因為大多數的人其實都不 Care 名詞解釋這回事

馬上來玩玩 就對了

但是玩之前必須要先申請 Facebook 應用程式

取得應用程式 ID (appId) 跟 應用程式密鑰 (appSecret)



申請過程沒啥好講的

但是如果我們在古早就 自行挑戰過 Facebook 登入的話...

WTF 介面改了 長得不一樣了

要先到 設定 這頁 點選 +新增平台



然後選網站



網站 URL 填好之後 切記! 要按 儲存



最近發現一個坑 那就是 只要網站沒有設定網域 (例如 localhost 或者 IP 的形式)

Facebook 只會讓應用程式擁有者 的帳號登入

用其他的帳號 一律看到這個 不明所以的錯誤



如果用 手工 javascript 的 FB.login() 的話更慘 會直接空白卡死



2014/02/26 補充:

後來發現原來是沙盒在作亂

http://stackoverflow.com/questions/20706322/how-to-disable-sandbox-mode-for-app-in-new-facebook-developer

在這頁可以切換...




MotherF___er!! 不能按是哪招??


後來經高人指點 才知道原來要填 E-mail 之後才能按 (X 鬼知道唷) 見圖2




再來 回到 VS 2013 建立一個 MVC 專案

他預設很好心的就準備好一堆範例程式 讓我們熟悉 他新攪和進來的東東

然後打開 \App_Start\Startup.Auth.cs

我們會發現只要取消註解 填上 appId 跟 appSecret 就結束了



此時登入頁面 就會多了 Facebook 登入的按鈕



如果點了 就GG了 那是因為我們使用 網址使用 localhost 然後又用了 該死的 IE



否則 點了會跳這個 不管問什麼都一定會按確定 的頁面



這時會回來網站問你...唉唷欸欸耶耶名字可以自己取耶呵呵呵呵呵呵



如果覺得有下底線的 username 很潮 我們會毫無意外的 獲得紅字



因為預設只接受英數字組合 中文也不行

修改 AccountController 的建構子 設定 AllowOnlyAlphanumericUserNames = false 就能解決

但是此時就沒任何規則 要加驗證要另外搞

public AccountController(UserManager<ApplicationUser> userManager)
{
    UserManager = userManager;
 
    UserManager.UserValidator = new UserValidator<ApplicationUser>(UserManager)
    {
        AllowOnlyAlphanumericUserNames = false
    };
}

然後就能快樂的登入了 可喜可賀



什麼? 這麼快? 快得令人覺得有些空虛 -.-

因為大部分都被黑箱了呀

好奇怎麼實作的話 就 reflector 催落起吧

AspNet Identity 玩弄手札 第一彈 Code First 自訂 profile data

One ASP.NET 表示:天下大勢,分久必合,合久必分

AspNet Identity 表示:我也不過是被 One ASP.NET 玩弄於股掌之間的一個犧牲者

老疆表示:程式設計師用一些點子來做一些無聊事情,這樣瞭不瞭

第一次聽到 AspNet Identity 是 老疆 的影片

http://www.youtube.com/watch?v=G2JC4KWTqXM&list=PLSvKFCIcFiosRfhmjcSCozircig3dRRrR&index=1

(11:15開始)

老疆的演講都很有梗

而且梗都是模仿不來的

別人說都不好笑 但是聽他講卻又有種獨特的幽默

我想這是一種 Aura









首先來玩 Micro$oft 最愛表演的戲碼 Code First + 自訂 profile data 神奇搞定會員登入功能

打開 VS 2013 建立一個 MVC 專案

他預設很好心的就準備好一堆範例程式 讓我們熟悉 他新攪和進來的東東

其實馬上就能執行了 可以註冊 登入 都沒問題



因為是 Code First 所以第一次呼叫 DbContext 的時候 會自動建立好 DB 跟 TABLE



以往要擴充 profile data 十分麻煩 有多麻煩我也不太瞭

因為相傳 業界公司 沒有人在用 都是自幹 會員機制 而我也不過是其中一人

現在他表明了 You can add profile data for the user by adding more properties to your ApplicationUser class

在這邊加屬性就搞定了

// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class ApplicationUser : IdentityUser
{
    public string Email { getset; }
 
    public int Point { getset; }
 
    public bool Gender { getset; }
 
    public DateTime Birthday { getset; }
}

原先的 RegisterViewModel 也順手加上 以利欄位驗證

public class RegisterViewModel
{
    [Required]
    [Display(Name = "使用者名稱")]
    public string UserName { getset; }
 
    [Required]
    [StringLength(100,
        ErrorMessage = "{0} 的長度至少必須為 {2} 個字元。",
        MinimumLength = 6
    )]
    [DataType(DataType.Password)]
    [Display(Name = "密碼")]
    public string Password { getset; }
 
    [DataType(DataType.Password)]
    [Display(Name = "確認密碼")]
    [Compare("Password"ErrorMessage = "密碼和確認密碼不相符。")]
    public string ConfirmPassword { getset; }
 
    [Required]
    [EmailAddress]
    [Display(Name = "E - mail")]
    public string Email { getset; }
 
    [Required]
    [Display(Name = "點數")]
    public int Point { getset; }
 
    [Required]
    [Display(Name = "性別")]
    public bool Gender { getset; }
 
    [DataType(DataType.Date)]
    [Display(Name = "生日")]
    public DateTime Birthday { getset; }
}

AccountController 的 Register 也要從 ViewModel 把值帶入 ApplicationUser

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser()
        {
            UserName = model.UserName,
            Email = model.Email,
            Point = model.Point,
            Gender = model.Gender,
            Birthday = model.Birthday
        };
 
        var result = await UserManager.CreateAsync(usermodel.Password);
        if (result.Succeeded)
        {
            await SignInAsync(userisPersistentfalse);
            return RedirectToAction("Index""Home");
        }
        else
        {
            AddErrors(result);
        }
    }
 
    // 如果執行到這裡,發生某項失敗,則重新顯示表單
    return View(model);
}

如果你覺得欄位很多很給掰 可以使用一些 Mapping 或者 Injecter 的套件

像是 Value Injecte



只要屬性名稱一致 就能無腦的搞定

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser().InjectFrom(modelas ApplicationUser;
 
        var result = await UserManager.CreateAsync(usermodel.Password);
        if (result.Succeeded)
        {
            await SignInAsync(userisPersistentfalse);
            return RedirectToAction("Index""Home");
        }
        else
        {
            AddErrors(result);
        }
    }
 
    // 如果執行到這裡,發生某項失敗,則重新顯示表單
    return View(model);
}

最後當然別忘了 View 可不會自動生出 HTML 也補上

<div class="form-group">
    @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Point, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        @Html.TextBoxFor(m => m.Point, new { @class = "form-control" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Gender, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        男
        @Html.RadioButtonFor(
            m => m.Gender, true, new { @id = "", @checked = "checked" })
        女
        @Html.RadioButtonFor(
            m => m.Gender, false, new { @id = "" })
    </div>
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Birthday, new { @class = "col-md-2 control-label" })
    <div class="col-md-10">
        @Html.TextBoxFor(m => m.Birthday, new { @class = "form-control" })
    </div>
</div>



搞定 馬上開心執行 立馬報錯 -.-



因為先前執行過一次已經 產生過 DB 跟 TABLE

但是這次 Code First 發現 schema 被異動過 無法 ORM 就跳錯了

請透過 套件管理主控台 來更新他

我們可以在 檢視/其他視窗/套件管理主控台 找到這貨

啟用轉移:Enable-Migrations -ContextTypeName MvcSample.ApplicationDbContext

轉移版本:Add-Migration '版本名稱'

更新資料庫:Update-Database

出現 Seed 就表示成功啦



這下搞定了 資料乖乖進 DB 可喜可賀



2014年2月7日 星期五

關於 LINQ to Entities 的時候 MSSQL 相等比較 預設不區分大小寫的問題

其實這個故事並不是發生在使用 LINQ to Entities 的時候

而是在 SSMS 敲 SQL 的時候

當時在 try 舊 DB 資料轉移新 DB 的整合的方法

說時遲那時快

這鬼才 DB 的 account 欄位的資料 竟然有在重複的 但是 key 不同

雖然登入是靠 SSO 的方式 但是 account 相同應該還是無法辨識才對

除了那些真正相同的 account 之外 忽然發現 大小寫不同 但是字串相同 也會被判斷成相同

估狗了一下才發現 原來 MSSQL 預設是沒在管大小寫的

跟啥 定序 之類的鬼東西有關

解法自然也有

1.直接改那個 定序 的設定

2.加上關鍵字 COLLATE Chinese_Taiwan_Stroke_CS_AS

參考







這種 MSSQL 才有的專屬天使

LINQ to Entities 應該絕對是會無視它的

立馬玩一下 果真如此

走訪 估狗 跟 stackoverflow 之後 解法約有二

1. .ToList() 之後再玩一次 因為這時候後就是 C# 物件 就認得大小寫

但是資料多的話肯定是 GG

2. 用之前的 T - SQL 方法 有很多鬼法子可以讓 LINQ to Entities 也吃 T - SQL

但是就跟 ORM 精神說掰掰



目前的選擇是 2 因為 1 讓人覺得實在多此一舉

但是要搞這種家醜不得外揚的東東

通常都會包成一個方法

有朝一日科技進步 或者 牛人出世 之際 再來改做法




雖然理想上我想要包成這種感覺

var q = db.user.UpperOrLowerCaseWhere(x => x.account == "amy");

讓人有種還是 LINQ 的錯覺

但是實際上有許多難點

1. 可能要反射來反射去 我才有辦法抓到 "account" 這個欄位名稱

2. 浪打返回布林 那我也無法抓到 "amy" -.-

3. 最後還是靠 T - SQL 作亂 如果在這行後面在加其他 LINQ 方法 肯定也是 GG





最後選擇擴充 IQueryable<T> 用法如下

var q = db.user.AsQueryable().UpperOrLowerCaseEquals(db"account""amy");

Where() 會轉成 IQueryable<T> 所以不必加 AsQueryable()

var q = db.user
    .Where(x => x.email.Length > 0)
    .UpperOrLowerCaseEquals(db"account""amy");

Extension 大概長這樣

    public static class IQueryableExtension
    {
        public static IEnumerable<TEntity> UpperOrLowerCaseEquals<TEntity>(
            this IQueryable<TEntity> query,
            DbContext db,
            string columnName,
            string keyword
        ) where TEntity : class
        {
            string sql = query.ToString();
            
            IObjectContextAdapter adapter = db as IObjectContextAdapter;
 
            ObjectContext objectContext = adapter.ObjectContext;
 
            var q = objectContext.ExecuteStoreQuery<TEntity>(
                string.Format(@"
                    SELECT * FROM ( {0} ) AS [table] 
                    WHERE [{1}] COLLATE Chinese_Taiwan_Stroke_CS_AS = @keyword ",
                    sql,
                    columnName
                ),
                new SqlParameter("@keyword"keyword)
            );
 
            return q.ToList();
        }
    }




命名為 UpperOrLowerCaseEquals 而非 UpperOrLowerCaseWhere

則是限制只用在相等比較

WHERE 要搞定的東東或許太複雜 ˊ_>ˋ



最後...牛人快出世吧

2013年12月31日 星期二

LINQ 封裝 "computed" properties 五行封印術 - 看技術文章學英文第二彈 - 跨年特別篇


警告:此篇為翻譯文章,所有內容皆為原作者所有
此外歡迎指正破英文,如果哪邊有嚴重謬誤哪肯定是我把靈魂出賣給了 google 小姐 ... 這篇好難翻呀 我日 -.-




警告:此篇文章 內容艱澀 技術過硬
這篇 100% 是技術人員寫的 英文用字的 可讀性 可維護性 就跟"大部分的專案" 一樣好




這篇文章的

server 指得是 SQL SERVER 這類的東東

client 指得是 自定義的 C# 代碼

translation 指得是 將 C# 轉成 查詢端語法





客戶端 自訂屬性 和 遠端 LINQ provider


如果你寫了些自訂屬性 你將無法在 遠端 LINQ 操作中 使用他們 (EX:LINQ to Entities)

問題發生的原因在於 這些客製屬性無法 轉譯到 server (EX:SQL SERVER)

因為他們已經被編譯成 中介語言 (intermediate language - IL)

而不是 個 需要被 IQueryable 實作 translation 的 LINQ 表達式樹 (LINQ expression trees)

目前在 .NET 是沒法度 用些反向工程 將 IL 變回方法 (methods) 和 語法 (syntax)

讓我們可以如預期的轉換成查詢語法 (remote query)

這意味者你不得不將查詢寫成兩個部分

首先是 server (EX:SQL SERVER) 可以幹的 (LINQ 查詢)

再用 ToList 或者 AsEnumerable 查詢出中間結果 到 client

然後就只能在這邊進行些運算操作

如果你想要 激烈的 做些轉換 這會非常耗損效能

我們 (David, Colin Meek and myself) 想出了一個 非關 provider 一次性宣告 屬性的方法

這樣他們就可以在兩種情況下使用

他能在 LINQ to SQL, LINQ to Entities 和任何其他 LINQ 輕易的運算屬性

在 .NET 3.5 SP1 運作良好 :)


使用前



這邊我們 擴充 Employee 類別 增加 Age 和 FullName

我們只需要 名字帶有 "da" 的人

但是為了 做出選取 我們不得不 抓下所有東東 到 client


partial class Employee {
 public string FullName {
  get { return Forename + " " + Surname; }
 }

 public int Age {
  get { return DateTime.Now.Year - BirthDate.Year -
   (((DateTime.Now.Month < BirthDate.Month)
   || DateTime.Now.Month == BirthDate.Month && DateTime.Now.Day < BirthDate.Day) ? 1 : 0));
  }
 }
}
...
var employees = db.Employees.ToList().Where(e => e.FullName.Contains("da")).GroupBy(e => e.Age);


使用後


這邊 使用我們的方式

他發生在 server 端...

而且 LINQ to SQL, LINQ to Entities 都行的通


partial class Employee {
    private static readonly CompiledExpression fullNameExpression
     = DefaultTranslationOf.Property(e => e.FullName).Is(e => e.Forename + " " + e.Surname);
    private static readonly CompiledExpression ageExpression
     = DefaultTranslationOf.Property(e => e.Age).Is(e => DateTime.Now.Year - e.BirthDate.Value.Year - (((DateTime.Now.Month < e.BirthDate.Value.Month) || (DateTime.Now.Month == e.BirthDate.Value.Month && DateTime.Now.Day < e.BirthDate.Value.Day)) ? 1 : 0)));

    public string FullName {
        get { return fullNameExpression.Evaluate(this); }
    }

    public int Age {
        get { return ageExpression.Evaluate(this); }
    }
}
...
var employees = db.Employees.Where(e => e.FullName.Contains("da")).GroupBy(e => e.Age).WithTranslations();


使用注意事項


上述技巧的注意事項是 你需要確保 你的類別 在查詢之前 有初始化 (看看下面的替代方案)

然而 很顯然的 你註冊的表達式(expression) 所指的 屬性(property) 必須能夠被 translated

所以你將需要約束你自己的方法(methods) 和 實作 IQueryable


這裡是一些上述例子的幾個替代方案。


註冊 表達式


你可以像這個例子一樣 在類別本身的註冊屬性

該屬性可以不靠任何反射 (reflection) 就能自行 計算表達式 (evaluate the expressions)

此外 如果 效能考量不是那麼重要的話

你可以在其他地方註冊它們

使用 方法 (methods) 透過反射 動態查看他們的值

例如
...
DefaultTranslationOf.Property(e => e.FullName).Is(e => e.Forename + " " + e.Surname);
var employees = db.Employees.Where(e => e.FullName.Contains("da")).GroupBy(e => e.Age).WithTranslations();
...
partial class Employee {
    public string FullName { get { return DefaultTranslationOf.Evaluate(this, MethodInfo.GetCurrentMethod());} }
}




如果 效能考量很重要

你可以獲得 重複運算的 完整代碼

一次從中介語言中的屬性 然後一次從 translation 的表達示

針對不同情境的不同對應


有時候 你的程式的某些部份 可能想在不同的情境下

使用不同的 translations 或 performance 等等

沒問題!

WithTranslations 方法 通常操作著預設的 translation 對應 (map)

(靠 DefaultTranslationOf 訪問)

但也有另一個使用 TranslationMap 的多載

讓你對付特定的情況

例如
var myTranslationMap = new TranslationMap();
myTranslationMap.Add(e => e.Name, e => e.FirstName + " " + e.LastName);
var results = (from e in db.Employees where e.Name.Contains("martin") select e).WithTranslations(myTranslationMap).ToList();



他是怎麼運作的


CompiledExpression<T, TResult>

我們需要做的第一件事是讓 自訂編寫的「計算」屬性

遠離 IL 然後 變回 表達式樹 (expression trees)

所以我們才能 translate 它們

因為我們也想 在 client 計算他們

所以我們需要在 執行時期 編譯他們

CompiledExpression 只是為了取得 表達式 Func<T, TResult> 而存在

允許編譯和計算 objects 針對不同的編譯版本

ExpressiveExtensions


這個小類別 提供 WithTranslations 擴充方法 和 internal TranslatingVisitor

以利走訪 透過 TranslationMap 實際註冊 Func<T, TResult> expressions 的 property

所以 底層的 LINQ provider 可以處理他

TranslationMap


我們需要有 編譯 表達式的 屬性對應

TranslationMap 為此存在

你可以手動建立一個 TranslationMap

將其傳入 WithTranslations

如果你想要依靠程式 在執行時期建立它們 或者 對不同情境使用不同方法

但是通常你會想用它...

DefaultTranslationOf


這個 helper 類別 讓你針對 default TranslationMap 註冊屬性

我們在 WithTranslations 沒有傳入值的時候使用

它也可以讓你查看 已註冊的屬性

所以你也可以計算他們

即使它耗費了一點反射的效能
public int Age { get { return DefaultTranslationOf.Evaluate(this, MethodInfo.GetCurrentMethod()); } }




有樂!


[)amien











最後...新年快樂 2014