Quantcast
Channel: ProgrammerXDB Blog - C#
Viewing all 73 articles
Browse latest View live

C# 6 新語法 - 1

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N150616102
出刊日期:2015/6/17
開發工具:Visual Studio 2015 RC
版本:.NET Framework 4.6、C# 6

在《Visual Studio 2015 IDE新功能》這篇文章中,介紹了Visual Studio 2015 RC版的IDE新功能,初窺工具所帶來的便利功能,而在這篇文章之中,我將介紹C# 6程式語言所提供的新功能。因為Visual Studio 2015中文版還未問市,因此在此篇文章之中,專有名詞的部分就儘量使用原文。

引用靜態成員(Using static members)

C# 5版之前,若使用using語法,只能夠引用到命名空間,若是為類別取個別名。若使用using語法引用命名空間,如此使用到此命名空間的類別就不用寫全名,例如以下程式,引用System之後,就可以直接使用Console類別,叫用它的方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CS6
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello");
        }
    }
}

而C# 6版的using static語法,可以引用到類別,我們將上列的程式碼改寫如下,在程式檔案上方,使用using和static關鍵字,引用到System.Console類別,後續程式碼就可以直接叫用Console類別的WriteLine方法,使得程式碼更為精簡:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
     WriteLine ( "Hello" );
    }
  }
}

 

在撰寫程式時,有很多常用的類別都可以比照辦理,像是DateTime、Math類別等等,例如以下程式碼片段引用了常用的Console、DateTime、Math類別等等:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
using static System.DateTime;
using static System.Math;
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      WriteLine ( "Hello" );
      WriteLine ( Now);
      WriteLine (PI);
    }
  }
}

 

當然使用此語法時要適當的自行處理不同命名空間下,有相同類別名稱的命名衝突問題。例如以下程式碼在A與B兩個命名空間下,都包含一個MyClass類別,其中有一個Print()方法,若同時使用using static語法引用到A與B命名空間,則在Main方法叫用到Print()方法時,這段程式碼將不能編譯:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static A.MyClass;
using static B.MyClass;
namespace ConsoleApplication1 {
  class Program {
    static void Main ( string [ ] args ) {
      Print ( );
    }
  }
}

namespace A {
  class MyClass {
   public static void Print() {
      Console.WriteLine ("A");
    }
  }
}

namespace B{
    class MyClass {
    public static void Print ( ) {
      Console.WriteLine ( "B" );
    }
  }
}

 

編譯的錯誤訊息如下:

clip_image002

圖 1:命名衝突錯誤。

Null conditional operator

在C#程式中若使用到Null物件進行轉型或某些邏輯判斷動作,可能會產生例外,導致程式無法順利執行,例如以下程式碼中定義一個Employee型別變數,初始化為Null,但下一行程式碼就使用到Employee型別的EmpName屬性:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApplication1 {
  class Program {
    static void Main( string[ ] args ) {
      Employee emp = null;
      Console.WriteLine( emp.EmpName );
    }
  }
  class Employee {
    public string EmpName;
  }
}

 

執行時,在上述的Console.WriteLine那行程式碼會因使用到未配置記憶體的物件,而產生例外錯誤,訊息請參考下圖所示:

clip_image004

圖 2:NullReferenceException錯誤。

我們需要在程式中進行判斷,在emp變數不為null的情況下,才存取Employee類別的EmpName屬性,修改程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
  class Program {
    static void Main ( string [ ] args ) {
      Employee emp = null;
      if ( emp != null ) {
        Console.WriteLine ( emp.EmpName );
      }

    }
  }
  class Employee {
    public string EmpName;
  }
}

 

在C# 6中可以使用  Null conditional operator 來解決這個問題,不必再自己判斷變數是否為Null,修改上面程式如下,只要一行程式碼,就可以判斷Null問題,再執行程式就不會有例外錯誤:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
  class Program {
    static void Main ( string [ ] args ) {
      Employee emp = null;
      Console.WriteLine ( emp?.EmpName );
    }
  }
  class Employee {
    public string EmpName;
  }
}


 

此外我們也可以搭配??運算子一起使用,在變數為Null時,直接設定預設值,改寫上例程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
  class Program {
    static void Main ( string [ ] args ) {
      Employee emp = null;
      var r = emp?.EmpName ?? "NoName";
      Console.WriteLine ( r );

    }
  }
  class Employee {
    public string EmpName;
  }
}

 

執行後,當emp為null時,便會印出預設值:

clip_image006

Null conditional operator 可以搭配事件的語法,來檢查事件是否被註冊,若事件已註冊,再觸發事件,在C# 5你需撰寫類似以下if的語法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
  class MyButton {
    public event System.EventHandler Click;
    public void OnClick ( EventArgs e ) {
      if ( Click != null ) {
        Click ( this , e );
      }
    }
  }
  class Program {
    static void Main ( string [ ] args ) {
      var btn = new MyButton();
      btn.Click += ( sender , e ) => {
        Console.WriteLine ( "Btn Click" );       
      };
      btn.OnClick(EventArgs.Empty);
    }
  }
}

 

 

範例中的MyButton類別包含一個Click事件,而onClick方法中判斷是否此事件被其它物件註冊,若有,才觸發Click事件。現在C# 6 可以這樣寫:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
  class MyButton {
    public event System.EventHandler Click;
    public void OnClick ( EventArgs e ) {
      Click?.Invoke ( this , e );
    }
  }
  class Program {
    static void Main ( string [ ] args ) {
      var btn = new MyButton();
      btn.Click += ( sender , e ) => {
        Console.WriteLine ( "Btn Click" );       
      };
      btn.OnClick(EventArgs.Empty);
    }
  }
}

 

此範例的執行結果如下圖所示:

clip_image008

圖 3:觸發事件。

字串格式化

在C# 5之前,若要格式化字串,常常使用到字串參數,例如以下字串中的{0}代表第一個參數,{1}代表第二個參數:

"Now is {0}:{1}"

以主控台程式為例,我們可以使用以下程式碼進行字串格式化,WriteLine方法的第二個參數值會代入{0}參數;而,WriteLine方法的第三個參數值會代入{1}參數:

using System;
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      var h = DateTime.Now.Hour;
      var m = DateTime.Now.Minute;
      Console.WriteLine ( "Now is {0}:{1}" , h , m ); //Now is 5:57
    }
  }
}

 

現在在C# 6之中,你可以直接在參數中引用變數的值,只要在字串前方加上 「$」符號,就可以使用到 h與m變數,並把h與m變數的值,直接代入{h}與{m}參數:

using System;
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      var h = DateTime.Now.Hour;
      var m = DateTime.Now.Minute;
      Console.WriteLine ( $"Now is {h}:{m}"); //Now is 5:57
    }
  }
}

此外也可以自訂顯示格式,例如以下程式碼,不足兩位數時前方將自動補「0」:

using System;
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      var h = DateTime.Now.Hour;
      var m = DateTime.Now.Minute;
      Console.WriteLine ( $"Now is {h:00}:{m:00}" ); //Now is 05:57
    }
  }
}

 

而以下的範例程式碼,展示貨幣的格式化語法,新的語法讓使用字串格式化的動作變得更為簡單:

using System;
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      double money =12345.67;
      Console.WriteLine ( $"Money is {money:c}" ); //Money is $12,345.67
      Console.WriteLine ( $"Money is {money:#,###.000}" ); //Money is 12,345.670
    }
  }
}

 

Index initializers

在C#中可以宣告Dictionary<T,T>類型的泛型集合,讓集合存放key=value配對出現的資料,後續可以根據key取得對應的值,例如以下範例程式碼定義一個key與value都為string型別的泛型集合,並叫用集合的Add方法新增兩個項目到集合之中,並根據ID、Name將對應的值讀出:

using static System.Console;
using System;
using System.Collections.Generic;

namespace ConsoleApplication1 {
  class Program {
    static void Main ( string [ ] args ) {
      var col = new Dictionary<string , string>( );
      col.Add ( "ID" , "1" );
      col.Add ( "Name" , "Mary" );
      WriteLine ( col [ "ID" ] );  //1
      WriteLine ( col [ "Name" ] ); //Mary
    }
  }
}


 

在C# 3可以使用Collection Initializer搭配Object Initializer,宣告集合那行順帶初始化集合成員,程式就變更短了,改寫上例程式碼如下:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1 {
  class Program {
    static void Main ( string [ ] args ) {
      var col =new Dictionary<string , string>( ) {
        { "ID", "1"},
          { "Name","Mary"}
      };

      Console.WriteLine ( col [ "ID" ] );  //1
      Console.WriteLine ( col [ "Name" ] ); //Mary
    }
  }
}

 

而在C# 6可以改用Index initializers來簡化語法,以下程式碼改寫上例,改用新語法達到相同效果,程式又更短了:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1 {
  class Program {
    static void Main ( string [ ] args ) {
      var col =new Dictionary<string , string>( ) {
        ["ID"] = "1",
        ["Name"] = "Mary"
      };

      Console.WriteLine ( col [ "ID" ] );  //1
      Console.WriteLine ( col [ "Name" ] ); //Mary
    }
  }
}

 

Exception Filters

Visual Basic、F#都已提供Exception Filters語法,現在C# 6也支援了。當你使用try..catch語法攔截例外錯誤時,可以在catch區段搭配 when關鍵字,設定篩選條件,以篩選要攔截的例外錯誤,參考以下程式碼範例:

using System;
using System.IO;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      int i = 0;
      try {
        int j = 10 / i;
        int k = int.Parse("x");
      
      } catch ( Exception ex ) when (ex.Message == "Input string was not in a correct format.") {
        Console.WriteLine ( "Format Error" );
      } catch ( Exception ex ) when (ex.Message == "Attempted to divide by zero.") {
        Console.WriteLine ( "Error" );
      }
    }
  }
}


C# 6 新語法 - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N150716201
出刊日期:2015/7/1
開發工具:Visual Studio 2015 RC
版本:.NET Framework 4.6、C# 6

在這篇文章之中,我將介紹C# 6程式語言所提供的新語法。因為Visual Studio 2015中文版還未問市,因此在此篇文章之中,專有名詞的部分就儘量使用原文。

Auto-Properties Initializers(自動屬性初始設定式)

C# 5版之前,若要為自動屬性設定值,你可能需要額外撰寫一些程式碼,例如透過建構函式來初始化自動屬性的值。參考以下範例程式碼,Employee類別中包含ID、Name兩個自動屬性,在建構函式中,設定它們的值:

using System;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      var emp = new Employee(1,"Mary");
      Console.WriteLine ( emp.ID );  //1
      Console.WriteLine ( emp.Name ); //Mary
    }
  }
  class Employee {
    public Employee ( ) {
    }
    public Employee ( int id , string name ) {
      ID = id;
      Name = name;
    }
    public int ID { get; set; }
    public string Name { get; set; }
  }
}


 

在C# 6版之後,使用自動屬性語法時,可以宣告順帶初始化屬性的值,只要宣告時加上「=」等號,再給個值就行了,參考以下範例程式碼:

using System;
using System.IO;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      var emp = new Employee();
      Console.WriteLine ( emp.ID );  //1
      Console.WriteLine ( emp.Name ); //Mary
    }
  }
  class Employee {
    public int ID { get; set; } = 1;
    public string Name { get; set; } = "Mary";
  }
}

 

唯讀自動屬性(Getter-only auto-properties)

在C# 5,自動屬性一定是可讀寫的,以下程式碼片段是C# 5的自動屬性語法,Company類別中包含一個Name的自動屬性,在定義時一定要宣告成可讀寫,也就是說一定要加入get與set存取子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      Company p = new Company();
      p.Name = "MyCompany";
      Console.WriteLine ( p.Name );
 
    }
  }
  class Company {
    public string Name { get; set; }
  }
 
}

 

若省略任何一個存取子,都會造成編譯錯誤,例如以下程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      Company p = new Company();
      p.Name = "MyCompany";
      Console.WriteLine ( p.Name );
 
    }
  }
  class Company {
    public string Name { get; }
  }
 
}

 

編譯時將出現錯誤訊息如下:

clip_image002

圖 1:在C# 5,自動屬性一定是可讀寫的。

在C# 6允許設計唯讀自動屬性,並利用 Auto-Properties Initializers 進行初始化,改寫上述範例程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      Company p = new Company();
      Console.WriteLine ( p.Name );

    }
  }
  class Company {
    public string Name { get; } = "MyCompany";
  }
}

 

同時也可和唯讀變數一樣,你可以在建構函式之中進行初始化的動作:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      Company p = new Company("MyCompany");
      Console.WriteLine ( p.Name ); // MyCompany
    }
  }
  class Company {
    public string Name { get; }
    public Company ( string n ) {
      Name = n;
    }
  }
}

 

除了在建構函式之中可以修改唯讀屬性的值之外,不能夠在其他程式碼試圖修改它的值,Visual Studio 2015 RC將會提示錯誤,請參考下圖所示:

clip_image004

圖 2:不能夠在其他程式碼試圖修改它的值。

Expression-bodied member

在C# 5我們可以為類別定義方法,例如以下程式碼為Employee類別定義一個GetName()方法:

using System;
class Program {
  static void Main( string[ ] args ) {
    Employee emp = new Employee( );
    emp.LastName = "Lee";
    emp.FirstName = "Mary";
    Console.WriteLine( emp.GetName( ) );  // Mary , Lee
  }
}
class Employee {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string GetName( ) {
    return FirstName + " , " + LastName;
  }
}

 

在C#6定義類別的方法(Method)與屬性(Property)時,可以直接使用Lambda Expression,讓程式又變短了,此語法稱之為Expression-bodied member。

將上述程式碼改用C# 6 Expression-bodied member語法撰寫如下,在「=>」符號右方直接回傳字串,連return關鍵字都可以省略:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      Employee emp = new Employee( );
      emp.LastName = "Lee";
      emp.FirstName = "Mary";
      Console.WriteLine ( emp.GetName ( ) ); //Mary , Lee
    }
  }
  class Employee {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName ( ) => FirstName + " , " + LastName;
  }
}

 

搭配C# 6 字串格式化功能來使用:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      Employee emp = new Employee( );
      emp.LastName = "Lee";
      emp.FirstName = "Mary";
      Console.WriteLine ( emp.GetName ( ) ); //Name is Mary , Lee
    }
  }
  class Employee {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName ( ) => $"Name is {FirstName} , {LastName}";
  }
}

 

C# 5之前,定義屬性時,要規規矩矩地加上get或set方法,例如以下FullName屬性:

using System;
class Program {
  static void Main( string[ ] args ) {
    Employee emp = new Employee( );
    emp.LastName = "Lee";
    emp.FirstName = "Mary";
    Console.WriteLine( emp.GetName( ) );  // Mary , Lee
    Console.WriteLine( emp.FullName ); // Mary , Lee
  }
}
class Employee {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string GetName( ) {
    return FirstName + " , " + LastName;
  }
  public string FullName {
    get {
      return this.FirstName + " , " + this.LastName;
    }
  }
 
}

 

而C# 6可以使用Lambda語法來改寫,稱之為Expression-bodied property,改寫上例程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      Employee emp = new Employee( );
      emp.LastName = "Lee";
      emp.FirstName = "Mary";
      Console.WriteLine ( emp.GetName ( ) ); // Mary , Lee
      Console.WriteLine (emp.FullName); // Mary , Lee
    }
  }
  class Employee {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName ( ) => FirstName + " , " + LastName ;   
    public string FullName => this.FirstName + " , " + this.LastName;         
  }
}

 

Nameof運算子

有時程式執行發生例外錯誤,我們想要提醒程式設計師錯誤是來自於一個參數,以便於程式除錯,如以下程式碼所示,在開啟檔案時,若發生找不到檔案的例外錯誤,在訊息中將提示fileName參數設定有誤:

using System;
using System.IO;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      OpenFile ( @"c:\temp\b.txt" );
    }

    static void OpenFile ( string fileName ) {
      if ( !File.Exists ( fileName ) ) {
        throw new FileNotFoundException ( "File Not Found! check fileName parameter" , fileName );
      } else {
        var s =File.ReadAllText ( fileName );
        Console.WriteLine ( s );
      }
    }
  }
}

 

程式在執行時若發生例外,將顯示以下的錯誤訊息:

Unhandled Exception: System.IO.FileNotFoundException: File Not Found! check file

Name parameter

參考執行結果如下圖:

clip_image006

圖 3:執行例外。

萬一有一天程式碼因進行最佳化或程式重構的關係,fileName變數名稱被改掉了,而例外錯誤訊息中忘了更改,此時印出的訊息就牛頭不對馬嘴,程式中並不存在一個fileName參數:

using System;
using System.IO;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      OpenFile ( @"c:\temp\b.txt" );
    }

    static void OpenFile ( string fs) {
      if ( !File.Exists ( fs ) ) {
        throw new FileNotFoundException ( "File Not Found! check fileName parameter" , fs );

      } else {
        var s =File.ReadAllText ( fs );
        Console.WriteLine ( s );
      }
    }
  }
}

 

為了避免這種情況,我們可以利用nameof運算子來取得變數的名稱,例如改寫程式碼如下:

using System;
using System.IO;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      OpenFile ( @"c:\temp\b.txt" );
    }

    static void OpenFile ( string fileName ) {
      if ( !File.Exists ( fileName ) ) {
        throw new FileNotFoundException ( "File Not Found! check "
          + nameof ( fileName ) + " parameter" , fileName );

      } else {
        var s =File.ReadAllText ( fileName );
        Console.WriteLine ( s );
      }
    }
  }
}

 

得到的錯誤訊息如下:

Unhandled Exception: System.IO.FileNotFoundException: File Not Found! check file

Name parameter

但若修改變數名稱為fs

using System;
using System.IO;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {

      OpenFile ( @"c:\temp\b.txt" );
    }

    static void OpenFile ( string fs ) {
      if ( !File.Exists ( fs ) ) {
        throw new FileNotFoundException ( "File Not Found! check "
          + nameof ( fs ) + " parameter" , fs );

      } else {
        var s =File.ReadAllText ( fs );
        Console.WriteLine ( s );
      }
    }
  }
}

 

得到的錯誤訊息能正確地反應:

Unhandled Exception: System.IO.FileNotFoundException: File Not Found! check fs parameter

同樣的,我們也可以搭配字串格式化的新語法:

using System;
using System.IO;

namespace CS6 {
  class Program {
    static void Main ( string [ ] args ) {
      OpenFile ( @"c:\temp\b.txt" );
    }
    static void OpenFile ( string fileName ) {
      if ( !File.Exists ( fileName ) ) {
        throw new FileNotFoundException ( $"File Not Found! check {nameof ( fileName )} parameter." );

      } else {
        var s =File.ReadAllText ( fileName );
        Console.WriteLine ( s );
      }
    }
  }
}

使用Azure AD帳戶登入MVC 5網站

$
0
0

.NET Magazine國際中文電子雜誌
者:許薰尹
稿:張智凱
文章編號:N160216801
出刊日期:2016/2/10
開發工具:Visual Studio 2015 Enterprise
資料庫:SQL Server Express 2014
版本:.NET Framework 4.6MVC5

ASP.NET MVC5網站支援多種驗證機制,在Visual Studio 2015建立MVC 5範本專案時,可以選擇以下的驗證方式:

  • · No Authentication :不驗證。
  • · Individual User Accounts:將個人帳號與密碼相關資訊寫到資料庫之中。
  • · Organizational Accounts:使用Azure AD或Office 365驗證。
  • · Windows Authentication:使用Windows帳號驗證。

本篇文章介紹如何使用Azure AD帳戶登入MVC 5網站的設計步驟。

 

建立目錄服務

首先你需要有Microsoft Azure的帳號,可以先從以下網址伸請免費試用一個月:

https://azure.microsoft.com/zh-tw/pricing/free-trial/

接下來我們需要在Microsoft Azures建立目錄服務,瀏覽到此網址https://azure.microsoft.com/zh-tw/,使用Microsoft Azures帳號登入。從下方選取「新增」-「應用程式服務」-「Active Directory」-「目錄」,建立Microsoft Azure AD目錄,請參考如下圖所示:

clip_image002

圖 1:建立Microsoft Azure AD目錄。

選取「自訂建立」,請參考如下圖所示:

clip_image004

圖 2:選取「自訂建立」Microsoft Azure AD目錄。

接著便會看到一個「加入目錄」的對話方塊,輸入適當的資訊。在此選取「建立新目錄」,為其設定一個名稱,以及網域名稱,網域名稱不能重複,然後按下右下方的確定按鈕,請參考如下圖所示:

clip_image006

圖 3:「加入目錄」。

稍待一會兒之後,目錄便建立完成,從Microsoft Azure網站Active Directory頁可以看到目前它處於作用中的狀態,請參考如下圖所示:

clip_image008

圖 4:目錄處於作用中的狀態。

建立目錄服務使用者

接下我們需為目錄服務先建立好管理者以及使用者帳號,以便後續進行測試的工作。從Microsoft Azures網站左方的管理畫面中選「Active Directory」,選取前一個步驟建立的「mvcdemodirectory」,點選右方管理頁面中的「使用者」,請參考如下圖所示:

clip_image010

圖 5:建立目錄服務使用者。

再點選下方的「加入使用者」,請參考如下圖所示:

clip_image012

圖 6:加入使用者。

設定一個全域系統管理者名稱,然後按一下右下方的箭頭,請參考如下圖所示:

clip_image014

圖 7:設定一個全域系統管理者名稱。

設定使用者設定檔案,設定使用者的角色為「全域管理員」,並給一個替代電子郵件地址,然後按一下右下方的箭頭,請參考如下圖所示:

clip_image016

圖 8:設定使用者的角色為「全域管理員」。

下一步會看到「取得暫時密碼」的畫面,按下「建立」按鈕,系統會先為你建立的帳號設定一個暫時性的密碼,在使用者第一次登入時,需輸入此暫時性密碼,當下便會要求變更密碼,請參考如下圖所示:

clip_image018

圖 9:「取得暫時密碼」。

接著便會看到暫時性的密碼顯示在畫面上,你可以點選密碼旁的複製按鈕將密碼複製到剪貼簿,然後按一下右下方確定的按鈕,請參考如下圖所示:

clip_image020

圖 10:複製密碼到剪貼簿。

建立使用者帳號

接著重複上面的步驟,建立使用者帳號,以下步驟建立一個名為Mary的使用者,然後按一下右下方的箭頭,請參考如下圖所示:

clip_image022

圖 11:建立使用者帳號。

設定使用者設定檔案,設定使用者的角色為「使用者」,並給一個替代電子郵件地址,然後按一下右下方的箭頭,請參考如下圖所示:

clip_image024

圖 12:設定使用者設定檔案。

下一步會看到「取得暫時密碼」的畫面,按下「建立」按鈕,系統會先為你建立的帳號設定一個暫時性的密碼,請參考如下圖所示:

clip_image026

圖 13:「取得暫時密碼」。

點選密碼旁的複製按鈕將密碼複製到剪貼簿,然後按一下右下方確定的按鈕,請參考如下圖所示:

clip_image028

圖 14:複製密碼到剪貼簿。

目前目錄中的帳號資料參考如下所示:

clip_image030

圖 15:目錄服務使用者清單。

建立MVC 5專案

目錄服務以及帳號建置完成之後,我們便可以利用Visual Studio 2015來建立MVC 5的網站,從「File」-「New」-「Project」,在「New Project」對話盒中,確認視窗上方.NET Framework的目標版本為「.NET Framework 4.6」,選取左方「Installed」-「Templates」-「Visual C#」程式語言,從「Web」分類中,選取「ASP.NET Web Application」,適當設定專案名稱與專案存放路徑,按下「OK」鍵,請參考如下圖所示:

clip_image032

圖 16:建立MVC 5專案。

在「New ASP.NET Project」對話盒中選取「MVC」,勾選下方的「MVC」項目,然後按一下畫面中的「Change Authentication」 按鈕,請參考如下圖所示:

clip_image034

圖 17:勾選下方的「MVC」項目。

選取「Work And School Accounts」,設定右方的清單方塊,選取「Cloud – Single Organization」項目,設定文章一開始在Microsoft Azures建立的網域名稱,請參考如下圖所示:

clip_image036

圖 18:選取「Cloud – Single Organization」。

此時會要求登入目錄服務,我們可以使用管理者的身份登入,請參考如下圖所示:

clip_image038

圖 19:登入目錄服務。

第一次登入時,會要求輸入密碼,以及要求變更密碼,請參考如下圖所示:

clip_image040

圖 20:變更密碼。

設定完成會回到專案建立的畫面,在對話盒中選取「MVC」,右下方會顯示目錄服務的資訊,然後按下「OK」按鈕建立專案,請參考如下圖所示:

clip_image042

圖 21:建立專案。

使用使用者帳號登入

MVC 5範本網站基本上包含了完整的驗證實作部分,也包含Home、About、Contact、Login、Register..等等幾網頁,因此只要執行網站就可以直接進行網站安全性的驗證測試。

從「Solution Explorer」視窗- 選取Views\Home資料夾下的Index.cshtml檔案,按CTRL+F5執行Index 檢視。因為HomeController類別的上方套用Authorize Attribute:

 

[Authorize]
public class HomeController : Controller

因此當想檢視首頁時,會被導向更入畫面,執行結果請參考如下圖所示,我們此時可以使用一般身份的使用者登入網站:

clip_image044

圖 22:登入目錄服務。

第一次登入時,會要求輸入密碼,以及要求變更與密碼,請參考如下圖所示:

clip_image046

圖 23:變更密碼。

按下「Accept」,請參考如下圖所示:

clip_image048

圖 24:登入目錄服務。

輸入密碼以登入,請參考如下圖所示:

clip_image050

圖 25:輸入密碼。

登入成功,首頁上將會看到歡迎使用者的訊息,請參考如下圖所示:

clip_image052

圖 26:登入成功,顯示首頁。

檢視專案產生的程式碼

最後讓我們檢視專案產生的程式碼,網站Web.Config檔案中的<appSettings>區段記錄了驗證相關資訊:

<appSettings>
  <add key="webpages:Version" value="3.0.0.0" />
  <add key="webpages:Enabled" value="false" />
  <add key="ClientValidationEnabled" value="true" />
  <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  <add key="ida:ClientId" value="6e85491f-e499-4c79-9b09-6cce3df51a16" />
  <add key="ida:AADInstance" value="https://login.microsoftonline.com/" />
  <add key="ida:Domain" value="mvcdemodirectory.onmicrosoft.com" />
  <add key="ida:TenantId" value="3fe65420-9351-4aba-819f-52f1df7901b6" />
  <add key="ida:PostLogoutRedirectUri" value="https://localhost:44300/" />
</appSettings>

 

在Startup.Auth.cs檔案Startup類別中,利用ConfigurationManager類別讀取這些資訊,並設定OWIN驗證的資訊:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Linq;
using System.Web;
using Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;

namespace MyADDemo {
  public partial class Startup {
    private static string clientId = ConfigurationManager.AppSettings [ "ida:ClientId" ];
    private static string aadInstance = ConfigurationManager.AppSettings [ "ida:AADInstance" ];
    private static string tenantId = ConfigurationManager.AppSettings [ "ida:TenantId" ];
    private static string postLogoutRedirectUri = ConfigurationManager.AppSettings [ "ida:PostLogoutRedirectUri" ];
    private static string authority = aadInstance + tenantId;

    public void ConfigureAuth( IAppBuilder app ) {
      app.SetDefaultSignInAsAuthenticationType( CookieAuthenticationDefaults.AuthenticationType );

      app.UseCookieAuthentication( new CookieAuthenticationOptions( ) );

      app.UseOpenIdConnectAuthentication(
          new OpenIdConnectAuthenticationOptions {
            ClientId = clientId ,
            Authority = authority ,
            PostLogoutRedirectUri = postLogoutRedirectUri
          } );
    }
  }
}


以下則是AccountController.cs登入、登出的程式碼。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.Owin.Security;

namespace MyADDemo.Controllers {
  public class AccountController : Controller {
    public void SignIn( ) {
      // Send an OpenID Connect sign-in request.
      if ( !Request.IsAuthenticated ) {
        HttpContext.GetOwinContext( ).Authentication.Challenge( new AuthenticationProperties { RedirectUri = "/" } ,
            OpenIdConnectAuthenticationDefaults.AuthenticationType );
      }
    }

    public void SignOut( ) {
      string callbackUrl = Url.Action( "SignOutCallback" , "Account" , routeValues: null , protocol: Request.Url.Scheme );

      HttpContext.GetOwinContext( ).Authentication.SignOut(
          new AuthenticationProperties { RedirectUri = callbackUrl } ,
          OpenIdConnectAuthenticationDefaults.AuthenticationType , CookieAuthenticationDefaults.AuthenticationType );
    }

    public ActionResult SignOutCallback( ) {
      if ( Request.IsAuthenticated ) {
        // Redirect to home page if the user is authenticated.
        return RedirectToAction( "Index" , "Home" );
      }

      return View( );
    }
  }
}

執行緒安全集合-不可變的集合

$
0
0

.NET Magazine國際中文電子雜誌
者:許薰尹
稿:張智凱
文章編號:N160216802
出刊日期:2016/2/24
開發工具:Visual Studio 2015 Enterprise
版本:.NET Framework 4.6

.NET Framework 4.5版新增一些不可變的集合物件,特別適用於同步或非同步的情況下使用。當集合中的內容不常變動,且想要以Thread Safe的方式操作集合時,便可以使用定義在 System.Collections.Immutable命名空間下的集合類別,由於它們是不可變的集合,只提供唯讀的存取機制,因此更節省記憶體的使用空間。本篇文章將介紹這些不可變的集合。

使用 Visual Studio 2015建立的範本專案中,預設並沒有參考包含這些類別的組件,因此你需要先利用Nuget工具下載它們。我們以Console專案為例子,使用Nuget下載System.Collections.Immutable套件:

Install-Package System.Collections.Immutable

這個命名空間下包含以下常用型別:

  • ImmutableArray與ImmutableArray<T>
  • ImmutableDictionary與ImmutableDictionary<TKey , TValue>
  • ImmutableSortedDictionary與ImmutableSortedDictionary<TKey , TValue>
  • ImmutableHashSet 與ImmutableHashSet<T>
  • ImmutableSortedSet與ImmutableSortedSet<T>
  • ImmutableList與ImmutableList<T>
  • ImmutableStack 與ImmutableStack<T>
  • ImmutableQueue與ImmutableQueue<T>

這些都屬於執行緒安全的集合(Threadsafe Collections)。

 

ImmutableArray與ImmutableArray<T>

以下程式碼使用ImmutableArray <T>建立存放int型別的集合,叫用Add方法新增三個項目到陣列之中,然後利用迴圈,叫用RemoveAt方法將所有項目印出之後,將此項目從集合中移除:

using System;
using System.Linq;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableArray<int> ary = ImmutableArray<int>.Empty;
      ary = ary.Add( 200 );
      ary = ary.Add( 100 );
      ary = ary.Add( 300 );

      var ary2 = ary.Add( 400 );

      Console.WriteLine( "ary" );
      while ( ary.Count( ) > 0 ) {
        Console.WriteLine( "\t" + ary [ 0 ] ); //印出200 100 300
        ary = ary.RemoveAt( 0 );
      }
      Console.WriteLine( "ary Count : {0} " , ary.Count( ) ); //0
      Console.WriteLine( );
      Console.WriteLine( "ary 2" );
      while ( ary2.Count( ) > 0 ) {
        Console.WriteLine( "\t" + ary2 [ 0 ] ); //印出200 100 300 400
        ary2 = ary2.RemoveAt( 0 );
      }
      Console.WriteLine( "ary2 Count : {0} " , ary2.Count( ) ); //0
    }
  }
}


程式碼中,利用Add 、RemoveAt方法新增、或移除ary陣列中的項目時,都會回傳變動過的集合,更新ary變數,原始的集合參考並不會變更,因此範例中讓ary2參照到ary陣列,此時ary與ary2兩個陣列會共用「200」、「100」、「300」項目佔有的記憶體,來增進效能。這個範例的執行結果,請參考下圖所示:

clip_image002

圖 1

所有不可變的集合(Immutable Collection)都具有以下的特色:

  • 不可變的集合之實體不會變動,因此它具備執行緒安全的特色。
  • 當你呼叫不可變集合的方法,原有集合不變動,方法將回傳一個異動的集合。
  • 不可變的集合適合用在共享狀態。

ImmutableDictionary與ImmutableDictionary<TKey , TValue>

ImmutableDictionary與ImmutableDictionary<TKey , TValue>集合中的項目,都是由一組鍵/值(Key/Value)配對項目所成的。ImmutableDictionary<TKey , TValue>的鍵值可以是任意型別。參考以下範例程式碼:

using System;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableDictionary<string , string> dics = ImmutableDictionary<string , string>.Empty;

      dics = dics.Add( "A01" , "Mary" );
      dics = dics.Add( "B01" , "Candy" );
      dics = dics.Add( "C01" , "LiLi" );

      Console.WriteLine( "Count = {0}" , dics.Count ); //3

      foreach ( var item in dics ) {
        Console.WriteLine( "Key : {0} , Value :{1} " , item.Key , item.Value );
        dics = dics.Remove( item.Key );
      }
      Console.WriteLine( "Count = {0}" , dics.Count ); //0

      Console.WriteLine( );
    }
  }
}


 

這個範例的執行結果,請參考下圖所示:

clip_image004

圖 2

ImmutableSortedDictionary與ImmutableSortedDictionary<TKey , TValue>

ImmutableSortedDictionary與ImmutableSortedDictionary<TKey , TValue>和這兩個類別相似,其中的項目會自動排序,例如修改上例程式碼,改用ImmutableSortedDictionary<string , string>:

 

using System;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableSortedDictionary<string , string> dics = ImmutableSortedDictionary<string , string>.Empty;

      dics = dics.Add( "A01" , "Mary" );
      dics = dics.Add( "B01" , "Candy" );
      dics = dics.Add( "C01" , "LiLi" );

      Console.WriteLine( "Count = {0}" , dics.Count ); //3

      foreach ( var item in dics ) {
        Console.WriteLine( "Key : {0} , Value :{1} " , item.Key , item.Value );
        dics = dics.Remove( item.Key );
      }
      Console.WriteLine( "Count = {0}" , dics.Count ); //0

      Console.WriteLine( );
    }
  }
}

這個範例的執行結果,請參考下圖所示:

clip_image006

圖 3

ImmutableHashSet 與ImmutableHashSet<T>

ImmutableHashSet 與ImmutableHashSet<T>適合用在儲存不重複的項目,在不常變動的情況下,可以讓多執行緒安全地存取。以下範例叫用六行Add方法新增6個項目到集合中,ImmutableHashSet會自動保留唯一項目,因此實際上放到集合中的項目只有3個:

using System;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableHashSet<int> set = ImmutableHashSet<int>.Empty;
      set = set.Add( 100 );
      set = set.Add( 300 );
      set = set.Add( 200 );
      set = set.Add( 100 );
      set = set.Add( 300 );
      set = set.Add( 200 );

      Console.WriteLine("Count = {0}", set.Count ); //3

      foreach ( var item in set ) {
        Console.WriteLine( item );
        set = set.Remove( item );
      }
      Console.WriteLine( "Count = {0}" , set.Count ); //0
    }
  }
}

 

這個範例的執行結果,請參考下圖所示:

clip_image008

圖 4

ImmutableSortedSet與ImmutableSortedSet<T>

ImmutableSortedSet、ImmutableSortedSet<T>與ImmutableHashSet 與ImmutableHashSet<T>非常類似,只多一個功能,ImmutableSortedSet、ImmutableSortedSet<T>會排序其中的項目,參考以下範例程式碼:

using System;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableSortedSet<int> set = ImmutableSortedSet<int>.Empty;
      set = set.Add( 100 );
      set = set.Add( 300 );
      set = set.Add( 200 );
      set = set.Add( 100 );
      set = set.Add( 300 );
      set = set.Add( 200 );
      Console.WriteLine( "Count = {0}" , set.Count ); //3

      foreach ( var item in set ) {
        Console.WriteLine( item );
        set = set.Remove( item );
      }
      Console.WriteLine( "Count = {0}" , set.Count ); //0

    }
  }
}

 

ImmutableSortedSet<T>會排序集合中的內容,這個範例的執行結果,請參考下圖所示:

clip_image010

圖 5

ImmutableList與ImmutableList<T>

ImmutableList與ImmutableList<T>可以利用索引存取集合中的項目,參考以下範例程式碼,使用Add方法新增一個項目到集合;使用AddRange新增一個陣列資料到集合。此外,還可以利用Insert方法,可以插入項目到指定的索引位置,索引以0開始計,參考以下範例程式碼:

using System;
using System.Linq;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableList<int> list = ImmutableList<int>.Empty;
      list = list.Add( 100 );
      list = list.AddRange( new int [ ] { 200 , 300 } );
      list = list.Insert( 0 , 400 );

      foreach ( var item in list ) {
        Console.WriteLine(item);
      }
    }
  }
}


 

這個範例的執行結果,請參考下圖所示:

clip_image012

圖 6

ImmutableList與ImmutableList<T>內部使用二元樹(Binary Tree)方式儲存資料,就效能上而言,應該儘量使用foreach來存取其中的項目,若使用一般的for迴圈雖然依舊可以將其中的項目取出,但效能會比foreach慢很多。

ImmutableStack與ImmutableStack<T>

堆疊是後進先出(last-in, first-out)的資料結構,以下程式碼使用ImmutableStack.Create<T>建立存放物件型別項目的堆疊,叫用Push方法新增三個項目到堆疊之中,然後叫用Pop方法取出三個項目:

using System;
using System.Linq;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableStack<object> stack = ImmutableStack.Create<object>( );

      stack = stack.Push( "mary" );
      stack = stack.Push( 1 );
      stack = stack.Push( DateTime.Now );

      while ( stack.Count( ) > 0 ) {
        object item;
        stack = stack.Pop( out item );
        Console.WriteLine( item );
      }
    }
  }
}

這個範例的執行結果,請參考下圖所示:

clip_image014

圖 7

以下程式碼建立存放int型別項目的堆疊,叫用Push方法新增三個數值型別的項目到堆疊之中,然後叫用Pop方法取出三個項目:

 

using System;
using System.Linq;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableQueue<int> q = ImmutableQueue<int>.Empty;
      q = q.Enqueue( 100 );
      q = q.Enqueue( 200 );
      q = q.Enqueue( 300 );

      while ( q.Count( ) > 0 ) {
        int item;
        q = q.Dequeue( out item );
        Console.WriteLine( item );
      }
    }
  }
}


這個範例的執行結果,請參考下圖所示:

clip_image016

圖 8

和之前的陣列例子一樣,參考以下範例程式碼,每次呼叫Push方法,新增一個項目到堆疊,就回傳變動過的集合,更新stack變數,原始的集合參考並不會變更,因此範例中讓stack2參照到stack堆疊,stack與stack2兩個堆疊會共用「200」、「100」、「300」項目佔有的記憶體,來增進效能。

using System;
using System.Linq;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {

      ImmutableStack<int> stack = ImmutableStack<int>.Empty;
      stack = stack.Push( 200 );
      stack = stack.Push( 100 );
      stack = stack.Push( 300 );

      var stack2 = stack.Push( 400 );

      Console.WriteLine( "Stack" );
      while ( stack.Count( ) > 0 ) {
        int item;
        stack = stack.Pop( out item );
        Console.WriteLine( "\t{0}" , item );
      }

      Console.WriteLine( "Stack 2" );
      while ( stack2.Count( ) > 0 ) {
        int item;
        stack2 = stack2.Pop( out item );
        Console.WriteLine( "\t{0}" , item );
      }
   }

  }
}

 

 

這個範例的執行結果,請參考下圖所示:

clip_image018

圖 9

ImmutableQueue與ImmutableQueue<T>

佇列和堆疊類似,但它是屬於先進先出(first-in, first-out)的資料結構,以下範例程式碼展現如何使用不可變的佇列:

using System;
using System.Linq;
using System.Collections.Immutable;
namespace ConsoleApplication2 {
  class Program {
    static void Main( string [ ] args ) {
      ImmutableQueue<int> q = ImmutableQueue<int>.Empty;
      q = q.Enqueue( 100 );
      q = q.Enqueue( 200 );
      q = q.Enqueue( 300 );

      while ( q.Count( ) > 0 ) {
        int item;
        q = q.Dequeue( out item );
        Console.WriteLine( item );
      }
    }
  }
}


這個範例的執行結果,請參考下圖所示:

clip_image020

圖 10

Tuple簡介

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N160517101

出刊日期:2016/5/4
開發工具:Visual Studio 2015 Enterprise Update 1
版本:.NET Framework 4.6、C# 6

.NET Framework 4版新增Tuple物件,Tuple是一個有序的資料結構,可以儲存異質物件。在這篇文章之中,將介紹Tuple物件的基本用法。

根據定義,Tuple類别總共有8個多載的Create方法,其定義表列如下:

namespace System {
  public static class Tuple {
    public static Tuple<T1> Create<T1>( T1 item1 );
    public static Tuple<T1 , T2> Create<T1, T2>( T1 item1 , T2 item2 );
    public static Tuple<T1 , T2 , T3> Create<T1, T2, T3>( T1 item1 , T2 item2 , T3 item3 );
    public static Tuple<T1 , T2 , T3 , T4> Create<T1, T2, T3, T4>( T1 item1 , T2 item2 , T3 item3 , T4 item4 );
     public static Tuple<T1 , T2 , T3 , T4 , T5> Create<T1, T2, T3, T4, T5>( T1 item1 , T2 item2 , T3 item3 , T4 item4 , T5 item5 );
     public static Tuple<T1 , T2 , T3 , T4 , T5 , T6> Create<T1, T2, T3, T4, T5, T6>( T1 item1 , T2 item2 , T3 item3 , T4 item4 , T5 item5 , T6 item6 );
     public static Tuple<T1 , T2 , T3 , T4 , T5 , T6 , T7> Create<T1, T2, T3, T4, T5, T6, T7>( T1 item1 , T2 item2 , T3 item3 , T4 item4 , T5 item5 , T6 item6 , T7 item7 );
     public static Tuple<T1 , T2 , T3 , T4 , T5 , T6 , T7 , Tuple<T8>> Create<T1, T2, T3, T4, T5, T6, T7, T8>( T1 item1 , T2 item2 , T3 item3 , T4 item4 , T5 item5 , T6 item6 , T7 item7 , T8 item8 );
  }
}

這些多載的Create方法,可以讓你選擇儲存一到七個項目,Create<T1>,表示可以儲存一個項目;Create<T1, T2>方法則可以儲存兩個項目;若要儲存超過8個以上的項目,則可以使用第八個多載方法,利用第八個引數,來建立巢狀式的Tuple物件。接下來讓我們來看看Tuple的應用。

 

使用Tuple讓方法回傳多個值

C#的方法(Method)中只能夠回傳一個運算過的值,若要回傳多個值,可以將多個值放在陣列或集合一次回傳,或者使用out參數來解決這個問題。但這些做法都有些缺點,使用陣列或集合一次回傳多個值會額外花費一些搜尋其中項目的時間;使用out參數需要額外宣告、初始化out變數。且以上的做法都無法確保執行緒安全(Thread-Safe)。現在Tuple是另一種讓方法回傳多個值的選擇,並且具備執行緒安全的特性。

我們來看看Tuple的應用,例如以下範例程式碼,在GetData方法之中,利用Tuple類別的Create方法,建立可以儲存兩個數值項目的Tuple物件,並將之傳回:

 

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      Tuple<int , int> result = GetData( );
      Console.WriteLine( $"SUM = {result.Item1}" );
      Console.WriteLine( $"Count = {result.Item2}" );
    }
    static Tuple<int , int> GetData( ) {
      int [ ] data = { 1 , 8 , 52 , 45 , 12 , 44 , 75 , 68 , 24 , 59 };
      return Tuple.Create( data.Sum( ) , data.Count( ) );
    }
  }
}

 

上述的GetData()方法計算data陣列中數值的總合,以及個數,並利用Tuple將這兩個計算過的數值利用Tuple回傳。在Main方法中我們可以利用屬性的語法(使用「.」符號),來存取Tuple中的項目。若要存取Tuple中的第一個項目,可以使用Item1屬性;若存取Tuple中的第二個項目,可以使用Item2屬性,依此類推。此範例程式的執行結果,請參考下圖所示:

clip_image002

圖 1:使用Tuple儲存兩個數值範例執行結果。

使用new關鍵字建立Tuple實體

除了使用Tuple類別的Create靜態方法來建立Tuple物件之外,你也可以直接使用new關鍵字,來建立Tuple物件的實體,並利用建構函式初始化它的內容,例如我們可以將上述範例程式碼可以改寫如下,並可得到相同的執行結果:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      Tuple<int , int> result = GetData( );
      Console.WriteLine( $"SUM = {result.Item1}" );
      Console.WriteLine( $"Count = {result.Item2}" );
    }
    static Tuple<int , int> GetData( ) {
      int [ ] data = { 1 , 8 , 52 , 45 , 12 , 44 , 75 , 68 , 24 , 59 };
      return new Tuple<int , int>( data.Sum( ) , data.Count( ) );
    }
  }
}

 

儲存八個以上的項目

若要在Tuple中儲存八個以上的項目,可以建立巢狀式的Tuple結構,例如以下範例程式碼所示,GetData方法中建立並回傳一個Tuple<int , int , int , int , int , int , int , Tuple<int>>物件,建構函式的第一到七個引數是int型別,第八個引數則是一個Tuple<int>物件。在Main方法中,我們利用Tuple的Rest屬性來取得最後一個Tuple<int>>物件:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      Tuple<int , int , int , int , int , int , int , Tuple<int>> result = GetData( );
      Console.WriteLine( $"Item1 = {result.Item1}" );
      Console.WriteLine( $"Item2 = {result.Item2}" );
      Console.WriteLine( $"Item3 = {result.Item3}" );
      Console.WriteLine( $"Item4 = {result.Item4}" );
      Console.WriteLine( $"Item5 = {result.Item5}" );
      Console.WriteLine( $"Item6 = {result.Item6}" );
      Console.WriteLine( $"Item7 = {result.Item7}" );
      Console.WriteLine( $"Item8 = {result.Rest.Item1}" );
    }
    static Tuple<int , int , int , int , int , int , int , Tuple<int>> GetData( ) {
      return new Tuple<int , int , int , int , int , int , int , Tuple<int>>( 1 , 2 , 3 , 4 , 5 , 6 , 7 , new Tuple<int>( 8 ) );
    }
  }
}

 

此範例程式的執行結果,請參考下圖所示:

clip_image004

圖 2:使用Tuple儲存八個以上的項目。

以下是使用Static方法來儲存八個以上項目的範例:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      Tuple<int , int , int , int , int , int , int , Tuple<int>> result = GetData( );
      Console.WriteLine( $"Item1 = {result.Item1}" );
      Console.WriteLine( $"Item2 = {result.Item2}" );
      Console.WriteLine( $"Item3 = {result.Item3}" );
      Console.WriteLine( $"Item4 = {result.Item4}" );
      Console.WriteLine( $"Item5 = {result.Item5}" );
      Console.WriteLine( $"Item6 = {result.Item6}" );
      Console.WriteLine( $"Item7 = {result.Item7}" );
      Console.WriteLine( $"Item8 = {result.Rest.Item1}" );
    }
    static Tuple<int , int , int , int , int , int , int , Tuple<int>> GetData( ) {
      return Tuple.Create( 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 );
    }
  }
}

 

使用Tuple物件當方法的參數傳遞

Tuple物件可以當做方法的參數來傳遞,這樣的好處是:你可以只宣告一個輸入參數,但可以傳遞不同個數與型別的資料到方法之中。這個設計比C# 語法的可變數目引數(params)來的方便,因為params參數限定只能使用相同的型別,而Tuple物件可以存放不同型別的資料。例如以下範例程式碼,建立一個可以存放兩個項目的Tuple物件,其中的第一個項目是數直型別;第二個項目則是字串型別:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      int [ ] data = { 1 , 8 , 52 , 45 , 12 , 44 , 75 , 68 , 24 , 59 };
      string s = $"Count = {data.Count( )}";
      Tuple<int , string> parm = Tuple.Create( data.Sum( ) , s );
      Print( parm );
    }
    static void Print( Tuple<int , string> p ) {
      Console.WriteLine( $"SUM = {p.Item1}" );
      Console.WriteLine( p.Item2 );
    }
  }
}

 

在Main方法中,我們將一個陣列中數值資料的總合,與一個字串打包在Tuple之中,並將之傳遞到Print方法之中。使用Tuple讓程式看起來更為簡潔。此範例程式的執行結果,請參考下圖所示:

clip_image006

圖 3:在Tuple物件儲存不同型別的資料。

再者,使用Tuple的好處是,它減少許多使用物件型別的Boxing與Unboxing的程序,使用強型別的方式來儲存資料,因此在開發工具之中,Visual Studio 便會自動提示Tuple其中項目的型別,例如上例的Item1會是數值型別(int),當你在Visual Studio 開發工具中,將滑鼠游標移動到變數上方,馬上就會顯示Tuple內項目的型別資訊;

clip_image008

圖 4:Visual Studio 開發工具自動辨識Tuple項目型別。

而Item2則是字串(string)型別:

clip_image010

圖 5:Visual Studio 開發工具自動辨識Tuple項目型別。

Tuple物件可以儲存特殊結構

我們可以在Tuple物件中儲存集合或陣列物件,來設計更特殊的資料結構,例如你同時想要儲存學生的姓名、年齡與興趣,興趣包含多種項目,你可以宣告如下的Tuple物件,利用List<string>來描述學生的興去有多個:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      var student = Tuple.Create(
       "Mary" ,
        28 ,
         new List<string>( ) { "walking" , "swimming" }
        );

      Console.WriteLine( student.Item1 );
      Console.WriteLine( student.Item2 );
      Console.WriteLine( "Interest :" );
      student.Item3.ForEach( s => Console.WriteLine( $"\t{s}" ) );
    }
  }
}

 

此範例程式的執行結果,請參考下圖所示:

clip_image012

圖 6:Tuple物件可以儲存特殊結構。

當然,你也可以自行設計一個類別來做到相同的事情,不過Tuple的好處是:它具備執行緒安全(Thread-Safe)的特性,你不需要煩惱這個問題。

在集合中儲存Tuple物件

我們也可以在集合之中儲存多個Tuple物件,例如以下範例程式碼,建立一個students集合,儲存Tuple<string , int , List<string>>型別的項目。利用集合的Add方法,加入多個Tuple物件到集合之中,最後使用foreach迴圈,將其中的項目的值印出:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      List<Tuple<string , int , List<string>>> students = new List<Tuple<string , int , List<string>>>( );

      students.Add( Tuple.Create(
       "Mary" ,
        28 ,
         new List<string>( ) { "walking" , "swimming" }
        ) );

      students.Add( Tuple.Create(
       "Candy" ,
        35 ,
         new List<string>( ) { "Playing video game" , "Climbing" }
        ) );

      foreach ( var item in students ) {
        Console.WriteLine( item.Item1 );
        Console.WriteLine( item.Item2 );
        Console.WriteLine( "Interest :" );
        item.Item3.ForEach( s => Console.WriteLine( $"\t{s}" ) );
        Console.WriteLine( "===============================" );
      }
    }
  }
}


 

此範例程式的執行結果,請參考下圖所示:

clip_image014

圖 7:在集合中儲存Tuple物件。

建立Tuple陣列

宣告Tuple陣列和宣告Tuple集合沒有什麼太大的不同,參考以下範例程式碼,建立三個Tuple<string>物件,儲存在陣列之中:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      Tuple<string> [ ] data = new Tuple<string> [ ] {
         Tuple.Create("Mary"),Tuple.Create("Candy"),Tuple.Create("Judy")
      };

      foreach ( Tuple<string> item in data ) {
        Console.WriteLine( item.Item1 );
      }
    }
  }
}


 

此範例程式的執行結果,請參考下圖所示:

clip_image016

圖 8:建立Tuple陣列。

排序Tuple項目

最後我們談談排序的問題,若要對集合或陣列中的項目進行排序,可以利用OrderBy(由小到大)或OrderByDescending(由大到小)擴充方法。以下範例程式碼,叫用OrderBy方法,根據學生的名稱排序,由小排到大,再利用for each 迴圈將其中的學生資料一一印出:

namespace TupleDemo {
  class Program {
    static void Main( string [ ] args ) {
      List<Tuple<string , int , List<string>>> students = new List<Tuple<string , int , List<string>>>( );

      students.Add( Tuple.Create(
       "Mary" ,
        28 ,
         new List<string>( ) { "walking" , "swimming" }
        ) );

      students.Add( Tuple.Create(
       "Candy" ,
        35 ,
         new List<string>( ) { "Playing video game" , "Climbing" }
        ) );

      foreach ( var item in students.OrderBy( t => t.Item1 ) ) {
        Console.WriteLine( item.Item1 );
        Console.WriteLine( item.Item2 );
        Console.WriteLine( "Interest :" );
        item.Item3.ForEach( s => Console.WriteLine( $"\t{s}" ) );
        Console.WriteLine( "===============================" );
      }
    }
  }
}


 

此範例程式的執行結果,請參考下圖所示:

clip_image018

圖 9:排序Tuple項目。

MongoDB入門 (3)

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N160717301
出刊日期:2016/7/13
資料庫:MongoDB v3.2.4、MongoDB.Driver v2.2.4

本篇文章將延續前兩篇《MongoDB入門 (1)》、《MongoDB入門 (2)》文章的內容,介紹如何透過.NET Framework,以C#程式碼來存取MongoDB資料庫中的內容,你可以利用MongoDB語言驅動程式所提供的屬性和分法,來查詢資料庫中集合內的文件,並針對集合中的文件進行新增、刪除、修改、與查詢的動作。

下載語言驅動程式

要在C#專案中存取MongoDB資料庫,需要透過MongoDB語言驅動程式,在本文撰寫時最新版本為2.2.4版,若要查詢最新版,可以參考官網網頁進行下載:

http://mongodb.github.io/mongo-csharp-driver/?_ga=1.148966769.674217283.1464248053

直接下載的網址如下:

https://github.com/mongodb/mongo-csharp-driver/releases/download/v2.2.4/CSharpDriver-2.2.4.zip

不過,若透過Visual Studio工具進行開發,可以直接使用工具進行下載。

建立C#專案

本文將介紹以主控台應用程式來存取MongoDB 資料。讓我們先啟動Visual Studio 2015開發環境,從「File」-「New」-「Project」,在「New Project」對話方塊中,「Templates」選取「Visual C#」-「Windows」分類,選取「Console Application」,建立一個主控台應用程式,並設定專案名稱:

clip_image002

圖 1:建立一個主控台應用程式。

使用Nuget套件管理員下載「MongoDb.Driver」函式庫。在「Solution Explorer」視窗選取專案名稱。從Visual Studio 2015開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入install-package指令,並使用「-version」指定安裝2.2.4版本:

install-packag MongoDB.Driver –version 2.2.4

另一種作法是使用圖型介面做安裝,在「Solution Explorer」視窗,點選專案,按滑鼠右鍵,從快捷選單選取「Manage NuGet Packages」選單:

clip_image004

圖 2:使用圖型介面安裝「MongoDb.Driver」函式庫。

接著,就會出現NuGet Package Manager視窗,在上方的文字方塊中輸入搜尋關鍵字,找到MongoDB.Driver套件後,從下拉式清單方塊選取想要安裝的版本,再點選右方的「Install」按鈕進行安裝(注意,你的電腦必需先連上網際網路,才能夠進行下載套件的動作):

clip_image006

圖 2:安裝MongoDB.Driver。

待相關的套件安裝完成之後,就可以開始撰寫程式碼了。

新增Document到集合

我們在主控台程式中加入以下程式碼,在程式上方先引用MongoDB.Bson、MongoDB.Driver等命名空間,在Main方法中加入程式:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            MongoClient client = new MongoClient("mongodb://127.0.0.1:27017/myTestDb");
            IMongoDatabase db = client.GetDatabase("myTestDb");
            var employees = db.GetCollection<BsonDocument>("employees");

            BsonDocument employee = new BsonDocument(){
                {"name", "mary"},
                { "age",40},
                { "email", "mary@test.com" },
                { "department", new BsonDocument(){
                    { "code", 100},
                    { "name", "Sales"}
                }
            }};

            employees.InsertOne(employee);
            Console.WriteLine("inserted!");
            Console.ReadLine();
        }
    }
}

 

範例中使用MongoClient類別來連接到MongoDB 伺服器,預設MongoClient會自動建立並管理連線集區(Connection Pool)。MongoClient類別的GetDatabase方法可以存取到指定的資料庫。若資料庫此時不存在也沒有關係,MongoDB會在第一次使用時,自動建立資料庫。IMongoDatabase介面的GetCollection<BsonDocument>方法可以取得指定的集合。取得集合物件之後,就可以新增文件到集合之中。我們建立一個BsonDocument物件代表要新增的文件,再叫用InsertOne方法,將文件新增到集合之中。InsertOne方法用來新增一份文件,它是以同步方式進行新增,另有一個InsertOneAsync方法,則支援非同步執行。若要新增多份文件,可以參考InsertMany方法(同步),或InsertManyAsync(非同步):方法。

當程式成功地將資料新增到資料庫後,我們可以利用mongo.exe工具連接到資料庫,觀察一下執行結果,使用use指令,連結到myTestDb資料庫,再叫用db.employees.find()方法,查詢集合中現有的文件:

C:\Program Files\MongoDB\Server\3.2\bin>mongo

MongoDB shell version: 3.2.4

connecting to: test

> use myTestDb

switched to db myTestDb

> db.employees.find()

{ "_id" : ObjectId("573edf31589eec01c0e77551"), "name" : "mary", "age" : 40, "email" : "mary@test.com", "department" : { "code" : 100, "name" : "Sales" } }

這個範例的執行結果,請參考下圖所示:

clip_image008

圖 3:查詢新增的資料。

查詢集合中的文件-非同步

FindAsync方法可以使用非同步的方式查詢集合中的文件,參考以下範例程式碼,叫用FindAsync方法取得Cursor,Cursor類似一個指標,可以讓你將查詢的文件一一讀取出來,範例中利用迴圈將文件讀出,只要叫用MoveNextAsync()方法,就移動Cursor到下一份文件,將文件內容印出。另外利用一個counter變數來計算資料的筆數:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Query();
            Console.ReadLine();
        }
        static async void Query() {
            MongoClient client = new MongoClient("mongodb://127.0.0.1:27017/myTestDb");
            IMongoDatabase db = client.GetDatabase("myTestDb");
            var employees = db.GetCollection<BsonDocument>("employees");

            var filter = new BsonDocument();
            var count = 0;
            using (var cursor = await employees.FindAsync(filter))
            {
                while (await cursor.MoveNextAsync())
                {
                    var batch = cursor.Current;
                    foreach (var document in batch)
                    {
                        Console.WriteLine(document);
                        count++;
                    }
                }
            }
            Console.WriteLine();
            Console.WriteLine($"Total Count : {count}");
            Console.ReadLine();
        }
     
    }
}

 

這個範例的執行結果,請參考下圖所示:

clip_image010

圖 4:使用Cursor讀取查詢的結果。

查詢集合中的文件-同步

Find方法可以使用同步的方式查詢集合中的文件。若想要取回某個集合中所有的文件,只要在叫用Find方法後,直接叫用ToList方法。待資料傳回之後,我們可以利用foreach迴圈,將文件一一讀出,叫用GetValue方法可以讀出文件中的欄位(Field),或是利用 [ ] 來讀取內容,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Query();
            Console.ReadLine();
        }
        static  void Query() {
            MongoClient client = new MongoClient("mongodb://127.0.0.1:27017/myTestDb");
            IMongoDatabase db = client.GetDatabase("myTestDb");
            var filter = new BsonDocument();
            var employees = db.GetCollection<BsonDocument>("employees").Find(new BsonDocument()).ToList();


            foreach (var employee in employees)
            {
                Console.WriteLine("_id : "+ employee.GetValue("_id"));
                Console.WriteLine("name : "+ employee["name"]);
                Console.WriteLine("age : " + employee["age"]);
                Console.WriteLine("email : " + employee["email"]);
                Console.WriteLine("department code : " + employee["department"]["code"]);
                Console.WriteLine("department name : " + employee["department"]["name"]);

                Console.WriteLine("==================================");
            }
            Console.ReadLine();
        }
      
    }
}

 

這個範例的執行結果,請參考下圖所示:

clip_image012

圖 5:讀取文件內容。

取代現有的文件

MongoDB提供多種資料修改的方法,包含同步、非同步方法。ReplaceOne(同步)與ReplaceOneAsync(非同步)可以代換掉集合中現有的文件。以下範例程式碼利用filter找尋「_id」為「573edf31589eec01c0e77551」的這份文件,然後叫用ReplaceOne方法將文件取代掉:

 

using System;
using System.Collections.Generic;

using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            MongoClient client = new MongoClient("mongodb://127.0.0.1:27017/myTestDb");
            IMongoDatabase db = client.GetDatabase("myTestDb");
            var employees = db.GetCollection<BsonDocument>("employees");

            BsonDocument employee = new BsonDocument(){
                {"name", "candy"},
                { "age", 50},
                { "email", "candy@test.com" },
                { "department", new BsonDocument(){
                    { "code", 200},
                    { "name", "Accounter"}
                }
            }};

            var filter = Builders<BsonDocument>.Filter.Eq("_id", new ObjectId("573edf31589eec01c0e77551"));
            employees.ReplaceOne(filter, employee);
            Console.WriteLine("Updated!");
            Console.ReadLine();
        }
    }
}

取代文件時,可以利用新文件順便新增欄位,或修改欄位的值,這個範例的執行結果,請參考下圖所示,在執行程式更新之前與之後的資料狀態如下:

clip_image014

圖 6:取代既有的文件。

更新文件

更新文件有許多方法可以叫用,UpdateOne(同步)與UpdateOneAsync(非同步)用來更新一份文件;UpdateMany(同步)與UpdateManyAsync(非同步)用來一次更新多份文件,若只想樣更新文件中部分欄位的值,可以利用Update方法,搭配Set方法設定欄位值,以下範例程式利用Update將「name」欄位設定為「LiLi」;將「age」設定為「20」;利用Filter篩選出 _id相符的資料,最後透過UpdateOne寫回資料庫:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            MongoClient client = new MongoClient("mongodb://127.0.0.1:27017/myTestDb");
            IMongoDatabase db = client.GetDatabase("myTestDb");
            var employees = db.GetCollection<BsonDocument>("employees");

            var update = Builders<BsonDocument>.Update
                .Set("name", "LiLi")
                .Set("age",20);

            var filter = Builders<BsonDocument>.Filter.Eq("_id", new ObjectId("573edf31589eec01c0e77551"));
            var result = employees.UpdateOne(filter, update);

            Console.WriteLine("Updated!");
            Console.ReadLine();
        }
    }
}

 

這個範例的執行結果,請參考下圖所示,在執行程式更新之前與之後的資料狀態如下:

clip_image016

圖 7:更新文件的欄位。

刪除文件

刪除一份文件可以利用DeleteOne(同步)或DeleteOneAsync(非同步)方法;DeleteMany(同步)與DeleteManyAsync(非同步)用來一次刪除多份文件。以下範例程式碼利用Filter篩選出_id相符的資料,然後將此份文件從資料庫刪除:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            MongoClient client = new MongoClient("mongodb://127.0.0.1:27017/myTestDb");
            IMongoDatabase db = client.GetDatabase("myTestDb");
            var employees = db.GetCollection<BsonDocument>("employees");

            var filter = Builders<BsonDocument>.Filter.Eq("_id", new ObjectId("573edf31589eec01c0e77551"));
            var result = employees.DeleteOne(filter);

            Console.WriteLine("Deleted! Delete Count :" + result.DeletedCount);
            Console.ReadLine();
        }
    }
}

這個範例的執行結果,請參考下圖所示,DeleteResult的DeletedCount屬性可以取得被刪除的資料筆數:

clip_image018

圖 8:資料刪除作業。

Change Tracking API - 1

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170418201
出刊日期: 2017/4/5

Entity Framework提供異動追蹤應用程式開發介面(Change Tracking API)來存取記憶體實體的資料是否有異動,例如新增、刪除或修改實體的屬性。透過這些API,可以進一步得知實體屬性目前的值(Current Values)、原始值(Original Values),以及資料庫最新的值(Database Values)。此外,還可以掌握實體中哪一個屬性被修改了。本文將介紹如何透過Entity Framework中的Change Tracking API來存取實體的資料。

本文的範例將採用Entity Framework的Change Tracking Proxy來偵測異動,利用Code First From Database,從現有的Pubs資料庫中建立ADO.NET實體資料模型。讓我們從建立一個主控台專案開始,利用Visual Studio 2015來建立Console Application(主控台應用程式),從「File」-「New」-「Project」,在「New Project」對話盒中,確認視窗上方.NET Framework的目標版本為「.NET Framework 4.6」或以上,選取左方「Installed」-「Templates」-「Visual C#」程式語言,從分類中,選取「Console Application」,適當設定專案名稱與專案存放路徑,按下「OK」鍵,請參考下圖所示:

clip_image002

圖 1:建立一個主控台專案。

使用反向工程建立ADO.NET實體資料模型

下一步是使用反向工程的功能來定義模型,我們想要從SQL Server的Pubs範例資料庫來建立模型。從Visual Studio 2015開發工具-「Solution Explorer」- 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」選項,開啟「Add New Item」對話盒,從右上方文字方塊輸入「ado.net」搜尋,選取「ADO.NET Entity Data Model」,設定名稱為「PubsContext」,按下「Add」按鈕,請參考下圖所示:

clip_image004

圖 2:使用反向工程建立ADO.NET實體資料模型。

然後選取「Code First from database」,建立ADO.NET實體資料模型,請參考下圖所示:

clip_image006

圖 3:建立ADO.NET實體資料模型。

在下一個畫面,選取「New Connection」建立資料庫連接,請參考下圖所示:

clip_image008

圖 4:建立資料庫連接。

按「Next」按鈕進入到下一個畫面,以選取資料庫物件,請參考下圖所示:

clip_image010

圖 5:選取資料庫物件。

當精靈完成之後,專案中資料夾內將產生個多個C#檔案,模型架構請參考下圖所示:

clip_image012

圖 6:部分的實體資料模型

偵測實體的異動

DbContext的Entry()方法可以存取異動追蹤(Change Tracking)資訊,Entry方法回傳一個DbEntityEntry物件,透過此物件的屬性和方法可以操做實體的資料。

舉例來說,當你透過前文的步驟建立實體資料模型後,目前專案中會包含一個store類別,store類別包含了兩個宣告為virtual 的導覽屬性(Navigation Property),參考程式碼如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public store()
        {
            sales = new HashSet<sale>();
            discounts = new HashSet<discount>();
        }

        [Key]
        [StringLength(4)]
        public string stor_id { get; set; }

        [StringLength(40)]
        public string stor_name { get; set; }

        [StringLength(40)]
        public string stor_address { get; set; }

        [StringLength(20)]
        public string city { get; set; }

        [StringLength(2)]
        public string state { get; set; }

        [StringLength(5)]
        public string zip { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<sale> sales { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<discount> discounts { get; set; }
    }
}

在主控台專案Program類別的Main()方法中撰寫以下程式碼,使用DbEntityEntry<TEntity>泛型型別的State屬性來得到目前實體的狀態是「Added」、「Unchanged」、「Modified」,或是「Deleted」。參考以下範例程式碼,先取回Stores資料表中stor_id為「6380」的資料,然後叫用DbContext類別的Entry()方法取回DbEntityEntry<store>物件,接著利用DbEntityEntry<store>類別的State屬性印出目前實體的狀態。下一行程式碼又修改stor_name屬性的值,再印出目前實體的狀態:

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine(entry.State);
                aStore.stor_name = "New Store Name";
                Console.WriteLine(entry.State);
            }
        }
    }
}

 

 

這個範例的執行結果會印出兩個Unchanged字串,請參考下圖所示:

clip_image014

圖 7:檢視狀態。

這是因為預設Code First From Database精靈會使用Snapshot Change Tracking模式來追蹤異動,我們需要手動叫用DbContext.ChangeTracker.DetectChanges方法來強制檢查是否有異動發生,修改程式碼如下:

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine(entry.State);
                aStore.stor_name = "New Store Name";
                context.ChangeTracker.DetectChanges();
                Console.WriteLine(entry.State);
            }
        }
    }
}

 

再次執行這個範例,則印出以下結果,從結果可得知修改完stor_name屬性之後,便偵測到資料已異動,而印出「Modified」狀態,請參考下圖所示:

clip_image016

圖 8:檢視狀態。

此外,根據文件的說明,當使用了以下的方法,就會觸發Entity Framework自動叫用DetectChanges()方法來偵測異動,因此大部分情況下,你不需要手動呼叫DetectChanges()方法:

  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.SaveChanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

使用Change Tracking Proxy

Entity Framework提供Change Tracking Proxy,使用Change Tracking Proxy的好處是,任何對實體異動都能即時得知,否則需要手動叫用DetectChanges()方法來強制檢查是否有異動發生。DbContext提供的方法有許多都會自動呼叫DetectChanges()方法,而Entry方法是其中的一個,不過若只是要讀取Entity的屬性值,並不會自動叫用DetectChanges()方法。

若要使用Change Tracking Proxy模型類別必需滿足以下的必要條件:

  • 類別必需標識為public,不可以是sealed類別。
  • 模型中每一個屬性需要宣告為virtual虛擬屬性。
  • 每一個屬性都必要要有標識為Public的getter與setter。
  • 任何集合型別的導覽屬性(Navigation Property),其型別必需為ICollection<T>。

讓我們修改store模型以讓Entity Frameowk建立Change Tracking Proxy,刪除(或註解)建構函式中兩行齣始化導覽屬性的程式碼,並且在每一個屬性宣告加上virtual關鍵字:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public store()
        {
            //sales = new HashSet<sale>();
            //discounts = new HashSet<discount>();
        }

        [Key]
        [StringLength(4)]
        public virtual string stor_id { get; set; }

        [StringLength(40)]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2)]
        public virtual string state { get; set; }

        [StringLength(5)]
        public virtual string zip { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<sale> sales { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

修改Main()方法:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine(entry.State);
                aStore.stor_name = "New Store Name";
                Console.WriteLine(entry.State);
            }
        }
    }
}

 

這次執行,如預期結果一樣,一開始實體的狀態是Unchanged,在修改stor_name屬性之後,狀態就變為「Modified」,請參考下圖所示:

clip_image017

圖 9:使用Change Tracking Proxy。

即使強制AutoDetectChangesEnabled屬性設定為「false」關掉自動偵測異動功能,修改程式碼如下,執行的結果和上例一樣:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine(entry.State);
                aStore.stor_name = "New Store Name";
                Console.WriteLine(entry.State);
            }
        }
    }
}

 

Current、Original與Database值

DbEntityEntry<T>類別提供了CurrentValues與OriginalValues兩個屬性可以取得目前屬性的值(CurrentValues)以及從資料庫查詢出來的屬性原始值(OriginalValues)。另外還有一個GetDatabaseValues()方法,可以取得目前資料庫資料的最新值。讓我們來試著利用程式碼,讀取Pubs資料庫Stores資料表資料,並觀察這些屬性的值,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine($"State : {entry.State}");
                Console.WriteLine("OriginalValues:");
                foreach (var propertyName in entry.OriginalValues.PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.OriginalValues[propertyName]}");

                }
                Console.WriteLine("====================================================");
                aStore.stor_name = "New Eric the Read Books 1";
                Console.WriteLine($"State : {entry.State}");
                Console.WriteLine("CurrentValues:");
                foreach (var propertyName in entry.CurrentValues.PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
                }

                Console.WriteLine("====================================================");
                context.Database.ExecuteSqlCommand("Update stores SET stor_name='New Eric the Read Books 2' WHERE stor_id='6380'");
                Console.WriteLine($"State : {entry.State}");
                Console.WriteLine("DatabaseValues:");

                foreach (var propertyName in entry.GetDatabaseValues().PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.GetDatabaseValues()[propertyName]}");
                }
            }
        }
    }
}

 

範例程式先利用Where()方法,從資料庫篩選出stor_id為「6380」的資料,接著叫用Single()方法取得代表此記錄的Store實體物件。跟著利用DbContext類別的Entry()方法,取得DbEntityEntry<store>物件。從DbEntityEntry<store>的State屬性我們可以知道第一次從資料庫將此筆資料取回時,store實體的狀態是「Unchanged」,第一個foreach迴圈將store實體所有屬性的OriginalValues印出,此時印出的值和資料庫目前的值是一致的。

下一段程式碼則將stor_name屬性修改為「New Eric the Read Books 1」,因此,如預期一般,從DbEntityEntry<store>的State屬性我們可以知道store實體的狀態是「Modified」,然後利用foreach迴圈將所有屬性的CurrentValues印出,目前stor_name屬性CurrentValues為「New Eric the Read Books 1」。

最後一段程式碼利用Database.ExecuteSqlCommand直接執行SQL語法,將Stores資料表6380這筆資料的Stor_name欄位值修改為「New Eric the Read Books 2」,然後再利用DbEntityEntry<store>類別的GetDatabaseValues()方法取得資料庫最新的值,此範例執行結果參考如下:

State : Unchanged

OriginalValues:

stor_id : 6380

stor_name : Eric the Read Books

stor_address : 788 Catamaugus Ave.

city : Seattle

state : WA

zip : 98056

====================================================

State : Modified

CurrentValues:

stor_id : 6380

stor_name : New Eric the Read Books 1

stor_address : 788 Catamaugus Ave.

city : Seattle

state : WA

zip : 98056

====================================================

State : Modified

DatabaseValues:

stor_id : 6380

stor_name : New Eric the Read Books 2

stor_address : 788 Catamaugus Ave.

city : Seattle

state : WA

zip : 98056

此範例執行結果請參考下圖所示:

clip_image019

圖 10:檢測狀態。

使用Reload()方法重新載入實體資料

有時使用者從資料庫載入實體資料(Entry)進行修改,後續又想要取消修改,那麼最簡單的方式便是從資料庫重新載入資料。DbEntityEntry包含一個Reload()方法可以從資料庫重新載入最新資料。

參考以下範例程式碼先載入資料庫stores資料表stor_id欄位為「6380」的記錄到store模型,叫用Entry()方法取得DbEntityEntry<store>物件,印出stor_name屬性目前的值,然後修改stor_name欄位的值為「New Eric the Read Books 1」,印出stor_name屬性目前的值。接著再叫用Reload()方法重新載入實體資料,再印出stor_name屬性目前的值:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                string curValue = entry.CurrentValues["stor_name"].ToString();

                Console.WriteLine($" CurrentValues : {curValue}");
                aStore.stor_name = "New Eric the Read Books 1";

                curValue = entry.CurrentValues["stor_name"].ToString();
                Console.WriteLine($" CurrentValues : {curValue}");

                context.Entry(aStore).Reload();

                curValue = entry.CurrentValues["stor_name"].ToString();

                Console.WriteLine($" CurrentValues : {curValue}");

            }
        }
    }
}

 

此範例執行結果如下,修改stor_name屬性之前,stor_nam屬性的值為「Eric the Read Books」,修改完之後,stor_nam屬性的值為「New Eric the Read Books 1」,叫用Reload()方法重新載入資料庫資料後,stor_nam屬性的值為「Eric the Read Books」:

CurrentValues : Eric the Read Books

CurrentValues : New Eric the Read Books 1

CurrentValues : Eric the Read Books

新增資料

新增實體資料時,只會保有Current值,沒有Original與Database值,若資料未新增到資料庫之前,試著讀取這兩個值將會得到例外錯誤。參考以下範例程式碼,範例執行時只能夠讀取CurrentValues,讀取OriginalValues與叫用GetDatabaseValues()方法時,會發生例外錯誤:

 

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store() { stor_id = "9999", stor_name = "9999 store" };
                context.stores.Add(aStore);

                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine($"State : {entry.State}");
                //context.SaveChanges();

                Console.WriteLine("OriginalValues:");
                foreach (var propertyName in entry.OriginalValues.PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.OriginalValues[propertyName]}");

                }

                Console.WriteLine("====================================================");
                Console.WriteLine($"State : {entry.State}");
                Console.WriteLine("CurrentValues:");
                foreach (var propertyName in entry.CurrentValues.PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
                }

                Console.WriteLine("====================================================");
                Console.WriteLine($"State : {entry.State}");
                Console.WriteLine("DatabaseValues:");

                foreach (var propertyName in entry.GetDatabaseValues().PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.GetDatabaseValues()[propertyName]}");
                }
            }
        }
    }
}

若將「context.SaveChanges();」這行程式碼的註解移除後執行,則會得到以下的執行結果,一開始建立一個新的store物件代表要新增的資料,並將之加入context.stores集合中,此時印出它的狀態為「Added」。

只要叫用SaveChanges()方法,則其狀態便會變成「Unchanged」,也可以得到Unchanged物件的CurrentValues與DatabaseValues,請參考下圖所示:

clip_image021

圖 11:檢視新增資料狀態。

新增資料-Change Tracking Proxy

特別要注意的是,使用new關鍵字來建立實體物件,並不會建立Change Tracking Proxy。 若想要使用Change Tracking Proxy,你可以改用DbSet類別提供的Create()方法來建立要新增的實體物件,參考以下程式碼範例,建立實體物件後,再利用add()方法加入DbContext:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Create<store>();
                aStore.stor_id = "9999";
                aStore.stor_name = "9999 store";

                context.stores.Add(aStore);

                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine($"State : {entry.State}");
                Console.WriteLine("CurrentValues:");
                foreach (var propertyName in entry.CurrentValues.PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
                }
                context.SaveChanges();
            }
        }
    }
}

 

範例程式執行時,使用除錯模式進行觀察,aStore的型別參考如下圖所示:

clip_image023

圖 12:Dynamic Proxy。

刪除資料

Entity Framework不記錄要刪除資料的CurrentValues,若試著讀取CurrentValues時,則會產生例外錯誤。參考以下範例程式碼建立一個store物件,stor_id屬性值為「9999」,代表要刪除的資料。

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store() { stor_id = "9999" };
                DbEntityEntry<store> entry = context.Entry(aStore);

                Console.WriteLine($"State : {entry.State}");
                context.stores.Attach(aStore);
                Console.WriteLine($"State : {entry.State}");
                context.stores.Remove(aStore);
                Console.WriteLine($"State : {entry.State}");

                Console.WriteLine("OriginalValues:");
                foreach (var propertyName in entry.OriginalValues.PropertyNames)
                {
                    Console.WriteLine($"\t{propertyName} : {entry.OriginalValues[propertyName]}");

                }

                Console.WriteLine("====================================================");
                Console.WriteLine($"State : {entry.State}");
                Console.WriteLine("CurrentValues:");
                foreach (var propertyName in entry.CurrentValues.PropertyNames) //Exception
                {
                    Console.WriteLine($"\t{propertyName} : {entry.CurrentValues[propertyName]}");
                }

            }
        }
    }
}

一開始建立store物件aStore的狀態是「Detached」;叫用Attach()方法加入DbSet<store>後狀態變更為「Unchanged」;叫用Remove方法,則狀態會變更為「Deleted」。因為Deleted物件無CurrentValues,所以上述範例在執行到最後一段foreach方法時,會產生例外錯誤。

 

刪除資料-Change Tracking Proxy

同樣地,若需要使用到Change Tracking Proxy,那麼必需利用DbSet的Create方法來建立物件,參考以下範例程式碼所示:

 

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Create<store>();
                aStore.stor_id = "9999";
                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine($"State : {entry.State}");
                context.stores.Attach(aStore);
                Console.WriteLine($"State : {entry.State}");
                context.stores.Remove(aStore);
                Console.WriteLine($"State : {entry.State}");
                context.SaveChanges();
            }
        }
    }
}

Change Tracking API - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170418201
出刊日期: 2017/4/19

本文延續《Change Tracking API - 1》一文的情境,介紹Entity Framework提供的異動追蹤應用程式開發介面(Change Tracking API)來讀取記憶體實體的資料是否有異動,並進一步利用這些API來修改實體或相關聯的資料。

 

使用GetValue<TValue>讀屬性值

前述《Change Tracking API - 1》一文的範例是利用索引子(Indexer)語法,利用 [ ] 中括號,括號中使用屬性名稱來讀取OriginalValues、CurrentValues以及資料庫的最新值,因使用索引子語法讀取出來的值其型別為object,所以你需要手動利用轉型的語法,將它轉換成適當型別,以利後續處理。除了利用索引子語法之外,還可以利用DbPropertyValues類別的GetValue<TValue>()方法來讀這些值,其中的TValue用來指定值的型別,這樣就不需要手動轉型。

參考以下範例程式碼,先載入資料庫stores資料表stor_id為「6380」的記錄到store實體,叫用Entry()方法取得DbEntityEntry<store>物件,然後設定stor_name的值為「New Eric the Read Books 1」,接著印出stor_name的CurrentValues、OriginalValues與DatabaseValue,因為stor_name屬性的型別為string,因此我們可以利用GetValue<string>("stor_name")語法來取得屬性值,並直接指定給string型別的變數:

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                aStore.stor_name = "New Eric the Read Books 1";

                Console.WriteLine($"State : {entry.State}");

                string oriValue = entry
                       .OriginalValues
                       .GetValue<string>("stor_name");

                Console.WriteLine($" OriginalValues : {oriValue}");

                string curValue = entry
                  .CurrentValues
                  .GetValue<string>("stor_name");

                Console.WriteLine($" CurrentValues : {curValue}");

                string dbValue = entry
                                 .GetDatabaseValues()
                                  .GetValue<string>("stor_name");

                Console.WriteLine($" DatabaseValues : {dbValue}");

            }
        }
    }
}

 

此範例的執行結果將印出以下值:

State : Modified

OriginalValues : Eric the Read Books

CurrentValues : New Eric the Read Books 1

DatabaseValues : Eric the Read Books

將DbPropertyValues值轉換成物件

為了方便透過DbPropertyValues來操作資料,DbPropertyValues類別提供一個ToObject()方法,可以將CurrentValues、OriginalValues與DatabaseValues直接轉換成一個物件,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                aStore.stor_name = "New Eric the Read Books 1";
                store curValue = (store)entry
                    .CurrentValues.ToObject();
                Console.WriteLine($" CurrentValues : {curValue.stor_name}");


                store oriValue = (store)entry
                    .OriginalValues.ToObject();
                Console.WriteLine($" OriginalValues : {oriValue.stor_name}");


                store dbValue = (store)context
                    .Entry(aStore)
                    .GetDatabaseValues()
                    .ToObject();

                Console.WriteLine($" DatabaseValues : {dbValue.stor_name}");

            }
        }
    }
}

 

ToObject()方法只會將純量屬性(Scalar Property)的值複製到新物件,不複製導覽屬性(Navigation Property)的值。此範例的執行結果將印出以下值:

CurrentValues : New Eric the Read Books 1

OriginalValues : Eric the Read Books

DatabaseValues : Eric the Read Books

 

使用DbPropertyValues修改屬性值

DbPropertyValues修改屬性值也可以用來修改屬性值,參考以下範例程式碼先載入資料庫stores資料表stor_id為「6380」的記錄,叫用Entry()方法取得DbEntityEntry<store>物件,然後利用索引子,設定stor_name的值為「Eric the Read Books」,接著印出stor_name的CurrentValues、OriginalValues與DatabaseValues:

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                entry.CurrentValues["stor_name"] = "Eric the Read Books";

                store curValue = (store)entry
                    .CurrentValues.ToObject();
                Console.WriteLine($" CurrentValues : {curValue.stor_name}");

                store oriValue = (store)entry
                    .OriginalValues.ToObject();
                Console.WriteLine($" OriginalValues : {oriValue.stor_name}");


                store dbValue = (store)context
                    .Entry(aStore)
                    .GetDatabaseValues()
                    .ToObject();

                Console.WriteLine($" DatabaseValues : {dbValue.stor_name}");
Console.WriteLine(aStore.stor_name);
                context.SaveChanges();
            }
        }
    }
}

 

此範例的執行結果將印出以下值,從結果可以看出,使用 CurrentValues修改stor_name,會連動修改stor_name屬性的值:

CurrentValues : Eric the Read Books

OriginalValues : New Eric the Read Books 1

DatabaseValues : New Eric the Read Books 1

Eric the Read Books

使用Change Tracking API的好處是,不需要叫用DetectChanges()方法,就可以偵測到屬性值的異動。舉例來說,修改上述程式碼,將AutoDetectChangesEnabled屬性設定為「false」關掉自動偵測異動功能,執行的結果和上例一樣,修改CurrentValues會連動修改stor_name的值:

 

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;

                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                entry.CurrentValues["stor_name"] = "New Eric the Read Books 1";

                store curValue = (store)entry
                    .CurrentValues.ToObject();
                Console.WriteLine($" CurrentValues : {curValue.stor_name}");

                store oriValue = (store)entry
                    .OriginalValues.ToObject();
                Console.WriteLine($" OriginalValues : {oriValue.stor_name}");


                store dbValue = (store)context
                    .Entry(aStore)
                    .GetDatabaseValues()
                    .ToObject();

                Console.WriteLine($" DatabaseValues : {dbValue.stor_name}");

                Console.WriteLine(aStore.stor_name);
                //context.SaveChanges();
            }
        }
    }
}

此範例執行結果如下所示:

CurrentValues : New Eric the Read Books 1

OriginalValues : Eric the Read Books

DatabaseValues : Eric the Read Books

New Eric the Read Books 1

 

使用SetValues()方法複製DbPropertyValues值

SetValues()方法可以複製DbPropertyValues的值。假設你想要提供一個功能,讓使用者選擇是否放棄目前修改的資料,那麼最簡單的作法,就是讓CurrentValues回復到OriginalValues原始值,然後將目前DbEntityEntry的狀態改為「Unchanged」。

參考以下範例程式碼,先取回Stores資料表中stor_id為「6380」的資料,然後叫用DbContext類別的Entry方法取回DbEntityEntry<store>物件,下一行程式碼修改stor_name屬性的值為「"New Eric the Read Books 1"」,接著根據使用者輸入的資料,若選擇取消修改(y),則叫用SetValues方法,將原始值複製到CurrentValues,否則則叫用SaveChanges()方法,將異動寫到資料庫:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {

                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);
                Console.WriteLine(aStore.stor_name);

                aStore.stor_name = "New Eric the Read Books 1";
                Console.WriteLine("Cancel Update? (y/n)");
                var r = Console.ReadLine();
                if (r.ToLower() == "y")
                {
                    entry.CurrentValues.SetValues(entry.OriginalValues);
                    entry.State = EntityState.Unchanged;
                }
                else {
                    context.SaveChanges();
                }
                Console.WriteLine(aStore.stor_name);
               
            }
        }
    }
}

 

SetValues()方法可以傳入DbPropertyValues物件,或是任意物件,根據你傳入SetValues()方法的物件屬性名稱做比對,複製名稱相符的屬性值。若傳入SetValues()方法的物件屬性型別和DbPropertyValues不相符,則產生例外錯誤。若傳入SetValues()方法的物件屬性不存在於DbPropertyValues物件,則此屬性將會被忽略。

此範例執行結果如下圖所示:

clip_image002

圖 1:取消更新。

Property()方法

Property()方法可以操做一個純量(Scalar)或複雜(Complex)屬性。我們可以利用Property()方法來讀寫屬性原始值(Original Value)與目前值(Current Value),也可以用來識別屬性的狀態是否為「Modified」。

參考以下範例程式碼先載入資料庫stores資料表stor_id為「6380」的記錄,叫用Entry()方法取得DbEntityEntry<store>物件,然後利用強型別的Property()方法,傳入Lambda運算式,設定stor_name的值為「New Eric the Read Books 1」,接著印出stor_name的CurrentValue、OriginalValue與IsModified屬性值:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {

                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                entry.Property(e => e.stor_name).CurrentValue = "New Eric the Read Books 1";
                Console.WriteLine($" Name : {entry.Property(e => e.stor_name).Name }");
                Console.WriteLine($" CurrentValue : {entry.Property(e => e.stor_name).CurrentValue }");
                Console.WriteLine($" OriginalValue : {entry.Property(e => e.stor_name).OriginalValue }");
                Console.WriteLine($" IsModified : {entry.Property(e => e.stor_name).IsModified }");
            }
        }
    }
}

 

此範例執行結果如下,因為變動了stor_name屬性,因此IsModified的屬性值會是「true」,如此方能知會Entity Framework在叫用SaveChanges()方法儲存資料時,要產生Update的SQL語法:

Name : stor_name

CurrentValue : New Eric the Read Books 1

OriginalValue : Eric the Read Books

IsModified : True

除了使用強型別的Property()方法,傳入Lambda運算式來存取屬性之外,還有一個弱型別版本的Property()方法,可以傳入屬性的字串型別名稱,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {

                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                entry.Property("stor_name").CurrentValue = "New Eric the Read Books 1";
                Console.WriteLine($" Name : {entry.Property("stor_name").Name }");
                Console.WriteLine($" CurrentValue : {entry.Property("stor_name").CurrentValue }");
                Console.WriteLine($" OriginalValue : {entry.Property("stor_name").OriginalValue }");
                Console.WriteLine($" IsModified : {entry.Property("stor_name").IsModified }");
            }
        }
    }
}

 

找尋有修改的屬性

弱型別版本的Property()方法有一個好用的功能,可以用來找尋實體中所有有修改的屬性。參考以下範例程式碼先載入資料庫stores資料表stor_id為「6380」的記錄,叫用Entry()方法取得DbEntityEntry<store>物件,然後利用弱型別的Property()方法,傳入屬性名稱,設定stor_name屬性的值為「New Eric the Read Books 1」;city屬性的值為「Portland」,接著利用LINQ查詢,找尋IsModified屬性為「true」的屬性名稱:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {

                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                entry.Property("stor_name").CurrentValue = "New Eric the Read Books 1";
                entry.Property("city").CurrentValue = "Portland";

                var result = from n in entry.CurrentValues.PropertyNames
                             where entry.Property(n).IsModified
                             select n;

                foreach (string name in result)
                {
                    Console.WriteLine(name);
                }
            }
        }
    }
}

 

此範例執行結果如下:

stor_name

city

使用Reference()方法存取導覽屬性

Reference()方法可以用來找尋導覽屬性(Navigation Property),為了說明Reference()方法,我們將使用Employee實體做範例,Employee程式如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    [Table("employee")]
    public partial class employee
    {
        [Key]
        [StringLength(9)]
        public virtual  string emp_id { get; set; }

        [Required]
        [StringLength(20)]
        public virtual string fname { get; set; }

        [StringLength(1)]
        public virtual string minit { get; set; }

        [Required]
        [StringLength(30)]
        public virtual string lname { get; set; }

        public virtual short job_id { get; set; }

        public virtual byte? job_lvl { get; set; }

        [Required]
        [StringLength(4)]
        public virtual string pub_id { get; set; }

        public virtual DateTime hire_date { get; set; }

        public virtual job job { get; set; }

        public virtual publisher publisher { get; set; }
    }
}

 

Job實體程式如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class job
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public job()
        {
        }

        [Key]
        public virtual short job_id { get; set; }

        [Required]
        [StringLength(50)]
        public virtual string job_desc { get; set; }

        public virtual byte min_lvl { get; set; }

        public virtual byte max_lvl { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<employee> employees { get; set; }
    }
}

 

參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aEmployee = context.employees.Where(e => e.emp_id == "PMA42628M").Single();
                DbEntityEntry<employee> entry = context.Entry(aEmployee);
                entry.Reference(j => j.job).Load();
                Console.WriteLine(entry.Reference(j => j.job).CurrentValue.job_desc);
            }
        }
    }
}

 

範例先從資料庫取出emp_id為「PMA42628M」的員工資料,叫用Entry()方法取得DbEntityEntry< employee >物件,然後利用Reference()方法,存取job導覽屬性。Load()方法用來明確從資料庫載入相關聯的job資料。最後使用DbReferenceEntry<TEntity, TProperty>的CurrentValue印出相關聯的Job之job_desc值,此範例執行結果印出如下的值:

Sales Representative

修改導覽屬性

修改DbReferenceEntry<TEntity, TProperty>的CurrentValue的值,可以用來修改資料表與資料表之間資料的關聯性。參考以下範例程式碼,先從資料庫取出emp_id為「PMA42628M」的員工資料,叫用Entry()方法取得DbEntityEntry< employee >物件,然後利用Reference()方法,存取job導覽屬性。接著再下一個查詢,找尋Jobs資料表job_id為「12」的資料,最後將新job物件指定給Reference(j => j.job).CurrentValue屬性,並叫用SaveChanges()方法儲存異動到資料庫:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aEmployee = context.employees.Where(e => e.emp_id == "PMA42628M").Single();
                DbEntityEntry<employee> entry = context.Entry(aEmployee);
                entry.Reference(j => j.job).Load();
                Console.WriteLine(entry.Reference(j => j.job).CurrentValue.job_desc);

                var newJob = context.jobs.Where(j => j.job_id == 12).Single();
                entry.Reference(j => j.job).CurrentValue = newJob;

                Console.WriteLine(entry.Reference(j => j.job).CurrentValue.job_desc);

                context.SaveChanges();

            }
        }
    }
}

 

此範例執行結果如下所示:

Sales Representative

Editor

檢視資料庫,PMA42628M員工的job_id欄位值變更為「12」,請參考下圖所示:

clip_image004

圖 2:修改導覽屬性。

使用Collection ()方法存取集合型別導覽屬性

若導覽屬性的型別是一個集合,那麼你可以使用Collection ()方法存取集合類型的導覽屬性。更進一步,可以再利用DbCollectionEntry<TEntity, TElement>類別的CurrentValue屬性來讀寫相關聯的集合物件。

回顧一下store模型包含一個discounts導覽屬性關聯到discount:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public store()
        {
            //sales = new HashSet<sale>();
            //discounts = new HashSet<discount>();
        }

        [Key]
        [StringLength(4)]
        public virtual string stor_id { get; set; }

        [StringLength(40)]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2)]
        public virtual string state { get; set; }

        [StringLength(5)]
        public virtual string zip { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<sale> sales { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

Discount實體程式如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class discount
    {
        [Key]
        [Column(Order = 0)]
        [StringLength(40)]
        public virtual string discounttype { get; set; }

        [StringLength(4)]
        public virtual string stor_id { get; set; }

        public virtual short? lowqty { get; set; }

        public virtual short? highqty { get; set; }

        [Key]
        [Column("discount", Order = 1)]
        public virtual decimal discount1 { get; set; }

        public virtual store store { get; set; }
    }
}

 

參考以下範例程式碼,先從資料庫取出stor_id為「6380」的商店資料,叫用Entry()方法取得DbEntityEntry< store >物件,然後利用Collection ()方法,存取discounts導覽屬性。從DbCollectionEntry<TEntity, TElement>類別的CurrentValue屬性取得集合,然後印出集合的Count筆數。

接著建立一個新discount物件,設定discounttype與discount,並加入context.discounts;最後叫用Add方法,將新建立的discount物件新增到entry.Collection(s => s.discounts).CurrentValue,並叫用SaveChanges()方法儲存異動到資料庫:

 

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = context.stores.Where(s => s.stor_id == "6380").Single();
                DbEntityEntry<store> entry = context.Entry(aStore);

                entry.Collection(s => s.sales).Load();
                int count = entry.Collection(s => s.discounts).CurrentValue.Count();
                Console.WriteLine($" discount count : {count}");

                discount newDiscount = new discount() { discounttype = "Special Discount", discount1 = 0.3m };
                context.discounts.Add(newDiscount);
                entry.Collection(s => s.discounts).CurrentValue.Add(newDiscount);
                count = entry.Collection(s => s.discounts).CurrentValue.Count();
                Console.WriteLine($" discount count : {count}");
                context.SaveChanges();

            }
        }
    }
}

此範例執行結果如下,資料表會新增一筆stor_id為「6380」的Special Discount記錄,請參考下圖所示:

clip_image006

圖 3:使用Collection ()方法存取集合型別導覽屬性。


Entity Framework Validation API - 1

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170518301
出刊日期: 2017/5/3

我們所設計的應用程式時常常需要搜集使用者輸入的資料,也需要檢查使用者輸入的資料是否正確符合需求。Entity Framework預設支援使用.NET Framework 4提供的ValidationAttribute、IValidatableObject來驗證實體模型的資料是否如預期,若預設的功能不符合需求,您也可以自行設計驗證機制。DbContext類別也新增了新的Validation API進一步整合並擴充驗證的功能。本篇文章將介紹如何在Entity Framework應用程式之中使用ValidationAttribute、IValidatableObject以及Validation API來設計驗證、自訂驗證,以及利用try..catch語法攔截驗證例外錯誤。

  • 預設執行以下動作,會促使DbContext進行驗證:
  • 呼叫DbContext.SaveChanges()方法,將驗證所有標識為Added與Modified的物件。
  • 呼叫DbEntityEntry.GetValidationResult()方法,將驗證特定物件。
  • 叫用DbContext.GetValidationErrors()方法,將驗證所有標識為Added與Modified的物件。

Store實體類別(Entity Class)

本文延續使用《Change Tracking API - 1》一文建立的ADO.NET實體資料模型來說明Entity Framework提供的驗證API。參考以下範例程式碼,目前Store實體類別(Entity Class)的定義如下,stor_id、stor_name、stor_address、city、state、zip屬性上方都套用了StringLength Attribute限定資料的有效長度。ValidationAttribute是.NET Framework 4 的功能,不是Entity Framework的一部分,但Entity Framework的Validation API已經整合了ValidationAttribute,將會根據套用的這些Attribute進行資料驗證檢查:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OROR";
                aStore.zip = "890766";

                if (context.Entry(aStore).GetValidationResult().IsValid)
                {
                    Console.WriteLine("Validation success!");
                }
                else
                {
                    Console.WriteLine("Validation failed!");
                }

            }
        }
    }
}

 

使用GetValidationResult()方法進行驗證

DbEntityEntry<T>類別的GetValidationResult()方法可以針對一個實體(Entity)的屬性資料進行驗證,它會根據實體設定的驗證Attribute進行資料有效性檢查,然後回傳一個DbEntityValidationResult物件代表驗證的結果,只要有任何一個屬性值違反驗證Attribute的規定,就會自動將DbEntityValidationResult物件IsValid屬性的值設定為「false」,代表驗證失敗;所有實體屬性驗證都成功的話,才會將IsValid屬性的值設定為「true」。

參考以下範例程式碼,建立一個store物件,故意設定stor_id屬性值設定為「99999」,其資料長度超過「4」;將state屬性的值設定為「9999」,資料長度超過「2」,zip屬性值設定為「890766」,資料長度超過「5」,然後判斷IsValid屬性來顯示驗證結果:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "9999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                if (context.Entry(aStore).GetValidationResult().IsValid)
                {
                    Console.WriteLine("Validation success!");
                }
                else
                {
                    Console.WriteLine("Validation failed!");
                }

            }
        }
    }
}

 

此範例執行結果如下所示,將印出驗證失敗的訊息,請參考下圖所示:

clip_image002

圖 1:使用GetValidationResult()方法驗證一個實體(Entity)的屬性。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "9999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                if (context.Entry(aStore).GetValidationResult().IsValid)
                {
                    Console.WriteLine("Validation success!");
                }
                else
                {
                    Console.WriteLine("Validation failed!");
                }

            }
        }
    }
}

 

修改目前程式碼進一步測試,填入有效的store屬性資料,讓資料不要超過預期的長度:

此範例執行結果如下所示:

clip_image004

圖 2:使用GetValidationResult()方法驗證一個實體(Entity)的屬性。

 

使用ValidationErrors屬性檢視詳細錯誤資訊

DbEntityEntry<T>類別的GetValidationResult()方法會回傳DbEntityValidationResult物件,此物件包含一個ValidationErrors屬性,可以進一步得知驗證錯誤的詳細資訊。ValidationErrors屬性是一個由DbValidationError物件所成的集合,DbValidationError物件包含兩個屬性:PropertyName,驗證錯誤的屬性名稱;ErrorMessage:驗證錯誤的錯誤訊息。

參考以下範例程式碼,叫用DbEntityEntry<T>類別的GetValidationResult()方法驗證新建立的store物件,故意設定stor_id屬性值設定為「99999」,其資料長度超過「4」;將state屬性的值設定為「9999」,資料長度超過「2」,zip屬性值設定為「890766」,資料長度超過「5」,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OROR";
                aStore.zip = "890766";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }

            }
        }
    }
}

 

此範例執行結果如下:

stor_id - The field stor_id must be a string with a maximum length of 4.

state - The field state must be a string with a maximum length of 2.

zip - The field zip must be a string with a maximum length of 5.

 

 

自訂錯誤訊息

ValidationAttribute包含一個ErrorMessage屬性,可以自定驗證錯誤訊息,參考以下範例程式碼,讓我們修改Store類別,為每一個StringLength Attribute加上自訂錯誤訊息,訊息中的{0}代表參數,代表屬性的名稱;{1}參數則是StringLength中設定的長度:

 

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_name { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_address { get; set; }

        [StringLength(20, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 
重新執行測試程式碼,範例執行結果如下所示:

stor_id - stor_id 長度不可超過 4

state - state 長度不可超過 2

zip - zip 長度不可超過 5

 

使用CustomValidationAttribute自訂驗證

如果預設的驗證Attribute不能滿足您複雜的商業邏輯需求,Entity Framework可以允許你使用CustomValidationAttribute在指定的屬性套用自訂驗證的邏輯。參考以下範例程式碼,包含一個MyCustomValidations靜態類別,裏頭定義一個ContentValidationRule靜態方法,此方法檢查指定的屬性值是否包含不合法的字串「admin」與「test」,若包含這兩個字串,則回傳一個ValidationResult物件,並設定錯誤訊息為「名稱不可包含 admin 或 test 字串」;若不包含這兩個字串,則回傳ValidationResult.Success:

 

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    public static class MyCustomValidations
    {
        public static ValidationResult ContentValidationRule(string value)
        {
            string errMsg = "";
            if (value != null)
            {
                if (value.Contains("admin") || value.Contains("test"))
                {
                    errMsg = "名稱不可包含 admin 或 test 字串";
                    return new ValidationResult(errMsg);
                }
            }
            return ValidationResult.Success;
        }
    }
}


接著修改store 類別程式碼,使用CustomValidation Attribute套用自訂驗證程式碼到store類別欲驗證的stor_name屬性上方,CustomValidation需要傳入兩個參數,第一個參數指定驗證程式碼所在的類別之型別,本例為「typeof(MyCustomValidations)」;第二個參數則是要叫用的方法名稱,本例為「ContentValidationRule」方法:

 

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40)]
        [CustomValidation(typeof(MyCustomValidations), "ContentValidationRule")]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

修改一下主程式碼來進行測試程式,將含有無效的admin字串填入stor_name屬性,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 admin store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OROR";
                aStore.zip = "890766";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }

            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示,除了顯示AttributeValidation的驗證錯誤資訊之外,也顯示了自訂驗證錯誤訊息:

stor_id - stor_id 長度不可超過 4

stor_name - 名稱不可包含admin或test字串

state - state 長度不可超過 2

zip - zip 長度不可超過 5

驗證特定屬性

DbPropertyEntry類別包含GetValidationErrors()方法,可以針對特定的屬性來進行資料驗證。GetValidationErrors()方法會回傳一個ICollection<DbValidationError>集合,包含多個驗證錯誤資訊。

參考以下程式碼範例,延續前文範例,建立一個store物件,並且故意填入長度超過40的字串到stor_name屬性中,且包含無效的admin字串,然後使用GetValidationErrors()方法驗證stor_name屬性,最後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 admin store 9999 admin store 9999 admin store 9999 admin store ";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                ICollection<DbValidationError> result =
                    context.Entry(aStore).Property(n => n.stor_name).GetValidationErrors();

                foreach (var item in result)
                {
                    Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                }
            }
        }
    }
}

 

根據我們設定的驗證規則,此範例執行結果如下所示:

stor_name - The field stor_name must be a string with a maximum length of 40.

stor_name - 名稱不可包含admin或test字串

Entity Framework Validation API - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170518302
出刊日期: 2017/5/17

本文延續《Entity Framework Validation API - 1》一文的說明,介紹Entity Framework驗證應用程式介面(Validation API)的基本應用。本文將探討類別階層驗證、驗證多個物件、攔截DbEntityValidationException例外錯誤與關閉驗證功能等議題。

 

類別階層驗證 – IValidatableObject介面

.NET Framework 4版新增一個IValidatableObject介面,提供類別階層的驗證能力。類別屬性資料若有相依關係,可以實作此介面來處理驗證邏輯。舉例來說,若撰寫一個訂返鄉火車票的功能,則去程日期必需小於回程日期,且額外又要要求回程日期必需要在去程日期的十天內。類似這種牽涉到多個屬性的資料檢查動作,就可以透過IValidatableObject介面來完成。

IValidatableObject介面包含一個Validate()方法,你可以在此方法加入自訂驗證邏輯。為了簡單起見,我們把前文提及的使用CustomValidationAttribute自訂驗證stor_name屬性的驗證程式碼,搬到類別階層,檢查指定的屬性值是否包含不合法的字串「admin」與「test」。參考以下程式碼範例,加入一個部分store類別,實作IValidatableObject介面的Validate()方法:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store : IValidatableObject
    {
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            string errMsg = "";
            if (stor_name != null)
            {
                if (stor_name.Contains("admin") || stor_name.Contains("test"))
                {
                    errMsg = "名稱不可包含admin或test字串";
                    yield return new ValidationResult(errMsg, new[] { "stor_name" });
                }
            }
        }
    }
    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

使用以下程式碼進行測試,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 admin store 9999  ";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }
            }
        }
    }
}

 

此範例執行結果如下所示,只有顯示出執行Attribute Validation驗證的錯誤訊息,並沒有觸發IValidatableObject的Validate()方法之驗證邏輯,:

stor_id - stor_id 長度不可超過 4

這是因為IValidatableObject只有在Attribute驗證通過之後,才會觸發驗證邏輯。讓我們修改測試程式碼如下,讓stor_id的長度不超過「4」,先通過資料驗證:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "9999";
                aStore.stor_name = "9999 admin store 9999  ";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }
            }
        }
    }
}

 

這次執行範例程式碼,範例執行結果如下所示:

stor_name - 名稱不可包含admin或test字串

 

類別階層驗證 - CustomValidationAttribute

除了IValidatableObject介面之外,類別階層驗證也可以透過CustomValidationAttribute來完成,讓我們修改store類別程式碼,讓它可以達到和上例IValidatableObject介面範例一樣的驗證功能。在store部分類別之中加入一個static方法 – TextValidationRule(),加入驗證邏輯,檢查指定的屬性值是否包含不合法的字串「admin」與「test」。因為一個ValidationAttribute只能回傳一個ValidationResult,若類別階層有多個驗證的條件,你可以撰寫多個方法針對不同規則來進行驗證。

最後只要在store類別上方套用CustomValidationAttribute傳入兩個參數,第一個參數指定驗證程式碼所在的類別之型別,本例為「typeof(store)」;第二個參數則是要叫用的方法名稱,本例為「TextValidationRule」方法,參考以下範例程式碼:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;


    [CustomValidation(typeof(store), "TextValidationRule")]
    public partial class store {
        public static ValidationResult TextValidationRule(store store, ValidationContext validationContext)
        {
            string errMsg = "";
            if (store.stor_name != null)
            {
                if (store.stor_name.Contains("admin") || store.stor_name.Contains("test"))
                {
                    errMsg = "名稱不可包含admin或test字串";
                    return new ValidationResult(errMsg, new[] { "stor_name" });
                }
            }
            return ValidationResult.Success;
        }
    }
    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

使用以下程式碼進行測試,建立一個store物件,並且故意填入無效的「admin」字串到stor_name屬性中,然後使用GetValidationErrors()方法驗證stor_name屬性,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "9999";
                aStore.stor_name = "9999 admin store 9999  ";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

stor_name - 名稱不可包含admin或test字串

 

驗證多個物件

有時新增或修改資料會牽涉到多個物件,在將資料寫到資料庫之前,我們可以使用DbContext類別的GetValidationErrors()方法一次檢查多個物件的資料是否有效,預設DbContext類別的GetValidationErrors()方法會驗證狀態為Added與Modified的物件。

以目前模型為例,模型包含store和discount,其關係如下圖所示:

clip_image002

圖 3:store和discount的關係。

參考以下範例程式碼,目前store類別的程式碼定義如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

參考以下範例程式碼,目前Discount類別的程式碼定義如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class discount
    {
        [Key]
        [Column(Order = 0)]
        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public  string discounttype { get; set; }

        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public  string stor_id { get; set; }

        public  short? lowqty { get; set; }

        public  short? highqty { get; set; }

        [Key]
        [Column("discount", Order = 1)]
        public virtual decimal discount1 { get; set; }

        public virtual store store { get; set; }
    }
}

 

參考以下程式碼範例加入測試程式,建立一個discount物件,將discount加入context.discounts屬性,故意填入長度超過「40」的字串到discounttype屬性;建立一個store物件,,將newDiscount加入store物件discounts屬性,故意讓stor_id屬性值的長度超過「4」個字。然後將aStore加入context.stores屬性,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                discount newDiscount = new discount()
                {
                    discounttype = "Special Discount Special Discount Special Discount Special Discount",
                    discount1 = 0.3m
                };
                context.discounts.Add(newDiscount);
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";
                aStore.discounts = new List<discount>() { newDiscount };

                context.stores.Add(aStore);

                foreach (var result in context.GetValidationErrors())
                {
                    Console.WriteLine(result.Entry.Entity.ToString());
                    foreach (DbValidationError error in result.ValidationErrors)
                    {
                        Console.WriteLine($" \t{error.PropertyName} - {error.ErrorMessage}");
                    }
                }

            }
        }
    }
}

 

此範例執行結果如下所示:

PubsDemo.store

stor_id - stor_id 長度不可超過 4

PubsDemo.discount

discounttype - discounttype 長度不可超過 40

 

攔截DbEntityValidationException例外錯誤

當你叫用DbContext類別的SaveChanges()方法試圖將新增或修改的資料寫回資料庫,Entity Framework會自動叫用GetValidationErrors()方法進行資料驗證。Entity Framework會驗證所有狀態為Added與Modified的實體。預設Entity Framework會驗證所有你套用在屬性上方的ValidationAttributes,以及叫用IValidatableObject的Validate()方法進行驗證,若發生驗證錯誤,將觸發DbEntityValidationException例外錯誤,並將錯誤放在EntityValidationErrors屬性中,其型別為IEnumerable<DbEntityValidationResult>。

參考以下程式碼範例,展示如何攔截DbEntityValidationException例外錯誤,範例先建立discount物件,故意讓newDiscount物件discounttype屬性值的長度超過40個字;接著建立store物件,故意讓stor_id屬性值的長度超過「4」個字:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                discount newDiscount = new discount()
                {
                    discounttype = "Special Discount Special Discount Special Discount Special Discount",
                    discount1 = 0.3m
                };
                context.discounts.Add(newDiscount);
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";
                aStore.discounts = new List<discount>() { newDiscount };

                context.stores.Add(aStore);
               
                try
                {
                    context.SaveChanges();
                }
                catch (DbEntityValidationException ex)
                {
                    foreach (var result in ex.EntityValidationErrors)
                    {
                        Console.WriteLine(result.Entry.Entity.ToString());
                        foreach (DbValidationError error in result.ValidationErrors)
                        {
                            Console.WriteLine($" \t{error.PropertyName} - {error.ErrorMessage}");
                        }
                    }
                   
                }
            }
        }
    }
}

因為資料驗證不通過,因此context.SaveChanges()這行程式碼一執行,就會產生例外誤,這兩筆資料將不會寫到資料庫之中。我們利用try..catch語法攔截DbEntityValidationException,從EntityValidationErrors屬性取得IEnumerable<DbEntityValidationResult>,從DbEntityValidationResult的ValidationErrors屬性取得DbValidationError,然後透過foreach將所有驗證錯誤的屬性名稱與錯誤訊息一一印出。

此範例執行結果如下所示,

PubsDemo.discount

discounttype - discounttype 長度不可超過 40

PubsDemo.store

stor_id - stor_id 長度不可超過 4

 

關閉驗證功能

預設Entity Framework會在你叫用DbContext物件的SaveChanges()方法時,自動叫用GetValidationErrors()方法,進行資料驗證的動作。Entity Framework會驗證所有利用ValidationAttribute與IValidatableObject介面定義的規則。若驗證不通過,將觸發DbEntityValidationException例外錯誤,你可以從例外錯誤物件的EntityValidationErrors屬性取得驗證結果。

有時在進行大量資料轉換的動作時,若已經能夠確保資料都是有效的,那麼在叫用SaveChanges()方法之前,關閉驗證的動作可以加快程式的執行效能。我們可以在DbContext類別的建構函式關閉驗證功能,參考以下範例程式碼,將Configuration.ValidateOnSaveEnabled設定為「false」:

namespace PubsDemo
{
    using System;
    using System.Data.Entity;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Linq;
    using System.Data.Entity.Infrastructure;

    public partial class PubsContext : DbContext
    {
        public PubsContext()
            : base("name=PubsContext")
        {
            Configuration.ValidateOnSaveEnabled = false;
        }

        public virtual DbSet<author> authors { get; set; }
        public virtual DbSet<employee> employees { get; set; }
        public virtual DbSet<job> jobs { get; set; }
        public virtual DbSet<pub_info> pub_info { get; set; }
        public virtual DbSet<publisher> publishers { get; set; }
        public virtual DbSet<sale> sales { get; set; }
        public virtual DbSet<store> stores { get; set; }
        public virtual DbSet<titleauthor> titleauthors { get; set; }
        public virtual DbSet<title> titles { get; set; }
        public virtual DbSet<discount> discounts { get; set; }
        public virtual DbSet<roysched> royscheds { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {

           //以下略


           
        }
    }
}

 

修改上個範例進行測試,於try..catch中增加一個catch區塊,攔截通用的Exception錯誤:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                discount newDiscount = new discount()
                {
                    discounttype = "Special Discount Special Discount Special Discount Special Discount",
                    discount1 = 0.3m
                };
                context.discounts.Add(newDiscount);
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";
                aStore.discounts = new List<discount>() { newDiscount };

                context.stores.Add(aStore);

                try
                {
                    context.SaveChanges();
                }
                catch (DbEntityValidationException ex)
                {
                    foreach (var result in ex.EntityValidationErrors)
                    {
                        Console.WriteLine(result.Entry.Entity.ToString());
                        foreach (DbValidationError error in result.ValidationErrors)
                        {
                            Console.WriteLine($" \t{error.PropertyName} - {error.ErrorMessage}");
                        }
                    }

                }
                catch (Exception ex)
                {

                    Console.WriteLine(ex.Message);
                }
            }
        }
    }
}

 

關閉驗證功能後,將不會觸發DbEntityValidationException例外錯誤,但因資料本身有問題,所以資料也無法寫到資料庫,此範例執行結果如下所示,將印出錯誤訊息:

An error occurred while updating the entries. See the inner exception for details.

另一種關閉驗證的方式是透過DbContext實體,參考以下範例程式碼,假設將上例建構函式Configuration.ValidateOnSaveEnabled這行程式碼註解:

   public partial class PubsContext : DbContext
    {
        public PubsContext()
            : base("name=PubsContext")
        {
            //Configuration.ValidateOnSaveEnabled = false;
        }
    //以下略
}

 

修改測試程式,設定ValidateOnSaveEnabled為「false」:

using (var context = new PubsContext())

{

context.Configuration.ValidateOnSaveEnabled = false;

//以下略

}

 

 

此範例執行結果同上例。

關於Entity Framework 查詢的二三事

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170518303
出刊日期: 2017/5/31

Entity Framework提供了LINQ to Entities,以讓程式設計師利用LINQ語法查詢資料庫的內容。LINQ to Entities提供了相當多的語法來載入資料,這些語法略有差異,了解這些不同語法的差異有助於撰寫效能更佳的應用程式。這篇文章將介紹一些常用的查詢語法,並了解它們的運用。

本文延續使用《Change Tracking API - 1》一文建立的ADO.NET實體資料模型來說明Entity Framework提供的查詢語法。

列舉DbSet物件查詢資料

每當你列舉DbSet物件中的内容,Entity Framework就會送出一個查詢到資料庫,載入資料庫資料表最新的資料。參考以下範例程式碼,查詢Pubs資料庫stores資料表資料,程式碼使用到兩個foreach方法,印出DbSet中store物件的屬性,這會促使Entity Framework下兩次資料庫查詢。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                foreach (var store in context.stores)
                {
                    Console.WriteLine(store.stor_name);
                }

                foreach (var store in context.stores)
                {
                    Console.WriteLine(store.stor_id);
                }

            }
        }
    }
}

 

你可以使用Visual Studio Diagnostic Tools來觀察程式的執行,從「Debug」->「Windows」->「Show Diagnostic Tools」,然後再按F5執行程式,執行到foreach方法,Entity Framework就送出以下查詢到資料庫

USE [pubs];

GO

SELECT
    [Extent1].[stor_id] AS [stor_id],
    [Extent1].[stor_name] AS [stor_name],
    [Extent1].[stor_address] AS [stor_address],
    [Extent1].[city] AS [city],
    [Extent1].[state] AS [state],
    [Extent1].[zip] AS [zip]
    FROM [dbo].[stores] AS [Extent1]


 

參考下圖,每一次執行到foreach列舉DbSet物件的內容時,就會攔截到一個送到資料庫的查詢事件:

clip_image002

圖 1:使用Diagnostic Tools監看程式執行。

若要考慮到執行效能,避免重複執行資料庫實體查詢的動作,可以善用LINQ提供的To開頭的方法,將查詢結果複製到記憶體。參考以下範例程式碼,叫用ToList()方法將資料複製到List<store>物件,後續便可以從此集合中找尋資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeList = context.stores.ToList();
                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_name);
                }

                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_id);
                }

            }
        }
    }
}

 

參考以下範例程式碼,則是使用ToArray()方法,將資料複製到store[]陣列之中,後續便可以從此陣列找尋資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeList = context.stores.ToArray();
                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_name);
                }

                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_id);
                }

            }
        }
    }
}

 

使用DbSet物件ToLookup()方法

因為Entity Framework GroupBy()方法回傳的是IQueryable<IGrouping<TKey, TSource>>泛型介面,IQueryable有延遲查詢(deferred execution)的特性,只要列舉此介面,就會建立並執行一個資料庫的實體查詢。以下這段程式碼,每次執行到foreach都會建立資料庫實際連線(以本例來說執行兩次),查詢資料庫資料。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeGroup = context.stores.GroupBy(s => s.state);
                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_name}");
                    }
                }

                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_id}");
                    }
                }
            }
        }
    }
}

 

每次列舉IQueryable會讓Entity Framework產生以下查詢讀取資料:

USE [pubs];

GO

SELECT
    [Project2].[C1] AS [C1],
    [Project2].[state] AS [state],
    [Project2].[C2] AS [C2],
    [Project2].[stor_id] AS [stor_id],
    [Project2].[stor_name] AS [stor_name],
    [Project2].[stor_address] AS [stor_address],
    [Project2].[city] AS [city],
    [Project2].[state1] AS [state1],
    [Project2].[zip] AS [zip]
    FROM ( SELECT
        [Distinct1].[state] AS [state],
        1 AS [C1],
        [Extent2].[stor_id] AS [stor_id],
        [Extent2].[stor_name] AS [stor_name],
        [Extent2].[stor_address] AS [stor_address],
        [Extent2].[city] AS [city],
        [Extent2].[state] AS [state1],
        [Extent2].[zip] AS [zip],
        CASE WHEN ([Extent2].[stor_id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
        FROM   (SELECT DISTINCT
            [Extent1].[state] AS [state]
            FROM [dbo].[stores] AS [Extent1] ) AS [Distinct1]
        LEFT OUTER JOIN [dbo].[stores] AS [Extent2] ON ([Distinct1].[state] = [Extent2].[state]) OR (([Distinct1].[state] IS NULL) AND ([Extent2].[state] IS NULL))
    )  AS [Project2]
    ORDER BY [Project2].[state] ASC, [Project2].[C2] ASC

 

為了效能,你可以改用DbSet物件ToLookup()方法,參考以下範例程式碼,只在ToLookup()這行程式執行一次實體資料庫查詢,將查詢結果放到記憶體,後續foreach語法的程式碼便可由記憶體中取得資料。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeGroup = context.stores.ToLookup(s => s.state);
                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_name}");
                    }
                }

                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_id}");
                    }
                }
            }
        }
    }
}

 

使用DbSet物件Find()方法

若想要找尋一個Entity Framework 放在記憶體中的物件,可以利用DbSet物件提供的Find()方法。Find()方法可以傳入key值當做搜尋的參數,找尋並回傳相符的物件,若找不到key相符的物件,Find()方法就回傳「null」。key值對應到主鍵(Primary)欄位。若key值是一個組合鍵,Find()方法可以按「,」符號區隔,填入組成key值的屬性名稱。

不是每一次叫用Find()方法時,都會從資料庫載入資料,而是按照以下優先順序來搜尋物件:

  • · 從記憶體找,搜尋DbSet物件中是否有包含key相符的Entity物件,並回傳此Entity物件。此Entity物件的資料可以從資料庫載入,或是一個新建立、附加到DbSet屬性,但尚未儲存到資料庫的Entity物件。
  • · 從資料庫載入Entity物件,並回傳此物件。

參考以下範例程式碼,找尋是否有key值為「6380」store資料,範例中建立DbContext物件,然後叫用DbSet的Find()方法找Entity。若使用「Visual Studio Diagnostic Tools」來觀察程式的執行,第一次執行Find()方法時,會建立實體資料庫連線,從資料庫載入資料。但第二次執行Find()方法時,就不會執行資料庫查詢,而是從記憶體將DbSet中key相符的物件回傳:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {

            using (var context = new PubsContext())
            {
                store s1 = context.stores.Find("6380"); // DB Query
                Console.WriteLine(s1.stor_name);

                store s2 = context.stores.Find("6380"); // From memory
                Console.WriteLine(s1.stor_name);

            }
        }
    }
}


 

Find()方法會讓Entity Framework產生以下查詢來取回資料,使用了「Select Top (2)」語法,因為Find()方法的參數對應到資料表主鍵欄位,不應該找回重複的兩筆資料,藉由「Select Top (2)」語法可以進行資料驗證的動作:

USE [pubs];

GO

--Type and value data was not available for the following variables. Their values have been set to defaults.

DECLARE @p0 AS SQL_VARIANT;

SET @p0 = NULL;

SELECT TOP (2)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE [Extent1].[stor_id] = @p0

我們看另一個例子,參考以下範例程式碼, 建立一個store物件,並將它加到context.stores之中,資料庫目前並沒有key值為「9999」的資料,因此兩個Find()方法實際上是直接搜尋記憶體,將新建立的store物件回傳,並沒有執行資料庫查詢。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {

            using (var context = new PubsContext())
            {
                store s = new store() { stor_id = "9999", stor_name = "9999 store" };
                context.stores.Add(s);
                store s1 = context.stores.Find("9999"); // From memory
                Console.WriteLine(s1.stor_name);

                store s2 = context.stores.Find("9999"); // From memory
                Console.WriteLine(s1.stor_name);

            }

        }
    }
}

 

使用DbSet物件SingleOrDefault()方法

假設每此找尋單一物件時,都要查詢資料庫的資料,則可以改用DbSet物件的Single()或SingleOrDefault()方法。Single()或SingleOrDefault()方法的差異是:Single()方法找不到條件相符的資料會產生例外錯誤;而SingleOrDefault()方法找不到條件相符的資料時不會產生例外錯誤,而是回傳「null」。

參考以下範例程式碼,叫用SingleOrDefault()方法找尋city等於「Seattle」的商店資訊,範例中叫用DbSet物件SingleOrDefault()方法兩次,使用Visual Studio Diagnostic Tools來觀察程式的執行,你將發現每次叫用時,都會執行資料庫查詢:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var q = from s in context.stores
                        where s.city == "Seattle"
                        select s;

                var r = q.SingleOrDefault(); //DB Query

                r = q.SingleOrDefault(); //DB Query

                Console.WriteLine(r.stor_name);
            }

        }
    }
}

 

SingleOrDefault()方法會讓Entity Framework產生以下SQL語法來查詢資料庫資料:

--The data may be truncated and may not represent the query that was run on the server

USE [pubs];

GO

SELECT TOP (2)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE 'Tapipei' = [Extent1].[city]

特別注意,Entity Framework使用了「Select Top (2)」語法來查詢資料,若查詢回傳兩筆紀錄,則SingleOrDefault()方法便會觸發System.InvalidOperationException例外錯誤,錯誤訊息為「Sequence contains more than one element」,請參考下圖所示:

clip_image004

圖 2:觸發System.InvalidOperationException例外錯誤。

只要叫用SingleOrDefault()方法,就會執行資料庫查詢,而不找尋新增到DbSet物件但尚未寫入資料庫的資料。參考以下範例程式碼,新建立一個Store物件,並將之加入DbSet物件中,但程式執行到SingleOrDefault()方法會回傳「null」,因此最後一行程式將會觸發NullReferenceException例外錯誤。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                store s = new store() { stor_id = "9999", stor_name = "9999 store" };
                context.stores.Add(s);
                var r = context.stores.SingleOrDefault(o => o.stor_name == "9999 store"); //DB Query
                Console.WriteLine(r.stor_name); // System.NullReferenceException'
            }
        }
    }
}

 

使用DbSet物件Single()方法

DbSet物件Single()方法和SingleOrDefault()方法運作方式大致相同,不再贅述,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var q = from s in context.stores
                        where s.city == "Seattle"
                        select s;

                var r = q.Single(); //DB Query

                r = q.Single(); //DB Query

                Console.WriteLine(r.stor_name);
            }

        }
    }
}

 

使用Single()方法將產生以下查詢:

USE [pubs];

GO

SELECT TOP (2)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE 'Seattle' = [Extent1].[city]

 

使用DbSet物件First()與FirstOrDefault ()方法

使用DbSet物件的Single()或SingleOrDefault()方法找詢資料時,若滿足篩選條件的資料有兩筆以上會觸發System.InvalidOperationException例外錯誤,若不在乎回傳資料的筆數,而想取得回傳資料的第一筆,可以改用DbSet物件的First()與FirstOrDefault ()方法。First ()與FirstOrDefault()方法的差異是:First ()方法找不到條件相符的資料會產生例外錯誤;而FirstOrDefault()方法找不到條件相符的資料時不會產生例外錯誤,而是回傳「null」。

只要叫用FirstOrDefault()方法,就會執行資料庫查詢,而不找尋新增到DbSet但尚未寫入資料庫的資料。參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var q = from s in context.stores
                        where s.city == "Seattle"
                        select s;

                var r = q.FirstOrDefault(); //DB Query

                r = q.FirstOrDefault(); //DB Query

                Console.WriteLine(r.stor_name);
            }
        }
    }
}

 

使用FirstOrDefault()方法將產生以下查詢:

USE [pubs];

GO

SELECT TOP (1)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE 'Seattle' = [Extent1].[city]

 

查詢本機資料

DbSet物件包含一個Local屬性,記錄從資料庫查詢回來的所有資料。此外此屬性也會記錄新增到DbSet物件但尚未寫回資料庫的Entity,不過並不會記錄被標示為刪除而實際上還存在於資料庫的Entity物件。

參考以下範例程式碼,一開始context.stores.Local.Count屬性的值是「0」,新增一個Store物件到DbSet物件,則Count屬性的值是「1」;使用FirstOrDefault()方法載入一筆資料,則Count屬性的值是「2」,從DbSet物件移除一個物件,則Count屬性的值是「1」。最後利用迴圈印出Local屬性所有的物件資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
                store s = new store() { stor_id = "9999", stor_name = "9999 store" };
                context.stores.Add(s);
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 1


                store storeToDelete = context.stores.FirstOrDefault(o => o.stor_id == "6380");
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 2

                context.stores.Remove(storeToDelete);

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 1

                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image006

圖 3:查詢本機資料。

載入資料到本機

若想要將資料庫資料表所有資料載入到本機,只要列舉DbSet物件就會將資料庫資料載入記憶體,並且轉換成Entity物件放在Local屬性中。參考以下範例程式碼,一開始context.stores.Local.Count屬性的值是「0」,使用foreach列舉DbSet物件store屬性,context.stores.Local.Count屬性的值便變為「6」;最後一段foreach將Local屬性中的物件資料印出:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
                foreach (var store in context.stores)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 6
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image008

圖 4:載入資料到本機。

除了使用列舉DataSet這招來載入資料之外,還有一個Load()方法可以使用`,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
                context.stores.Load();
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 6
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image010

圖 5:使用Load()方法載入資料。

若不想一次載入資料表所有資料,Load()方法可以搭配LINQ查詢,參考以下範例程式碼,載入state為「CA」的資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0

                var q = from s in context.stores
                        where s.state == "CA"
                        select s;

                q.Load();

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 3
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} - {store.state}");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image012

圖 6:使用Load()方法載入部分資料表資料。

我們可以叫用Load()方法多次,分批將資料載入,參考以下範例程式碼,第一次Load()方法載入三筆State為「CA」的資料;第二個Load()方法載入筆State為「WA」的資料,第二次叫用Load()方法載入資料時,不會清空Local屬性,新查詢出來的資料會附加到Local屬性:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0

                var q = from s in context.stores
                        where s.state == "CA"
                        select s;

                q.Load();

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 3
                q = from s in context.stores
                        where s.state == "WA"
                        select s;
                q.Load();

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 5
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} - {store.state}");
                }
            }
        }
    }
}


執行測試程式碼,此範例執行結果如下所示:

clip_image014

圖 7:使用Load()方法批次載入資料。

使用列舉與旗標設計多選

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N170618401
出刊日期: 2017/6/14

Flags Attribute可以應用在Enum型別多選的情況,通常是在列舉代表一堆旗標(Flag)所成的集合時使用,可以表達一個以上的值。這種類型的列舉型別會搭配位元運算子來操作(bitwise operator)。我們只要在Enum型別套用System.FlagsAttribute attribute,列舉值則以2的倍數來定義,就可以搭配AND、OR、NOT、XOR位元運算子來操作。

 

使用Flags定義列舉

例如以下程式碼,定義一個列舉,描述影片撥放時段,可能是白天(Day)、晚上(Night)、平日(WeekDay)或假日(Holiday),列舉上方套用[Flags] Attribute:

[Flags]
public enum ShowOptions
{
    Day = 1,
    Night = 2,
    WeekDay = 4,
    Holiday = 8
}

只要列舉值是2的倍數就可以運作,上述的程式碼等同於使用以下位元運算子計算出的值:

[Flags]
public enum ShowOptions
{
    Day = 1 << 0,
    Night = 1 << 1,
    WeekDay = 1 << 2,
    Holiday = 1 << 3
}

也可以等同於以下程式碼:

[Flags]
public enum ShowOptions
{
    Day = 1 << 0,
    Night = 1 << Day,
    WeekDay = 1 << Night,
    Holiday = 1 << WeekDay
}

若是C# 7可以使用「0b」開始來表達位元資料:

[Flags]
public enum ShowOptions {
  Day =     0b00000001,
  Night =   0b00000010,
  WeekDay = 0b00000100,
  Holiday = 0b00001000
}

 

使用列舉

定義完列舉之後,我們就可以在程式中這樣使用,參考以下主控台範例程式碼,宣告一個myOptions變數,使用「|」(OR)運算子設定兩個值Day與Night:

class TestClass
{
    static void Main(string[] args)
    {
        ShowOptions myOptions = ShowOptions.Day | ShowOptions.Night;
        Console.WriteLine(myOptions); //Day, Night
        Console.WriteLine((int)myOptions); //3
    }
}

在底層會進行如下的位元運算,得到myOptions的值為「3」,請參考下圖所示:

clip_image002

圖 1

我們可以利用And(&)運算子,進行位元運算來判斷myOptions是否包含特定的列舉值:

if ((myOptions & ShowOptions.Day) == ShowOptions.Day)
{
    Console.WriteLine("myOptions包含Day");
}
if ((myOptions & ShowOptions.Night) == ShowOptions.Night)
{
    Console.WriteLine("myOptions包含Night");
}
if ((myOptions & ShowOptions.WeekDay) == ShowOptions.WeekDay)
{
    Console.WriteLine("myOptions包含WeekDay");
}
if ((myOptions & ShowOptions.Holiday) == ShowOptions.Holiday)
{
    Console.WriteLine("myOptions包含Holiday");
}

這段程式碼運算的結果會輸出:

 

myOptions包含Day
myOptions包含Night

以這段程式碼為例,測試目前的myOptopns是否包含Day:

if ((myOptions & ShowOptions.Day) == ShowOptions.Day)
{
    Console.WriteLine("myOptions包含Day");
}

myOptions目前為0011,Day為0001,做And(&)運算完得到0001 (同Day),因此只要And運算完的結果等同於列舉值,就表示有包含此列舉值,請參考下圖所示:

clip_image004

圖 2

換句話說,參考以下範例程式碼,測試目前的myOptopns是否包含Day,我們可以如此改寫之,And運算完的結果若不為0則表示包含其值:

if ((3 & 1) != 0)
{
    Console.WriteLine("myOptions包含Day");
}

 

And運算完的結果,請參考下圖所示:

clip_image006

圖 3

依此類推,以下這段程式碼,測試目前的myOptopns是否包含Night::

if ((3 & 2) != 0)
{
    Console.WriteLine("myOptions包含Night");
}

 

And運算完的結果,請參考下圖所示:

clip_image008

圖 4

以下這段程式碼,測試目前的myOptopns是否包含Weekday:

if ((3 & 4) != 0)
{
    Console.WriteLine("myOptions包含Weekday");
}

 

And運算完的結果,請參考下圖所示:

clip_image010

圖 5

 

以下這段程式碼,測試目前的myOptopns是否包含Holiday::

if ((3 & 8) != 0)
{
  Console.WriteLine("myOptions包含Holiday");
}

And運算完的結果,請參考下圖所示:

clip_image012

圖 6

 

Enum型別也包含一個HasFlag方法,可以用來判斷是否包含特定的值,我們簡化上述程式碼,程式改寫如下:

if (myOptions.HasFlag(ShowOptions.Day))
{
    Console.WriteLine("myOptions包含Day");
}
if (myOptions.HasFlag(ShowOptions.Night))
{
    Console.WriteLine("myOptions包含Night");
}
if (myOptions.HasFlag(ShowOptions.WeekDay))
{
    Console.WriteLine("myOptions包含WeekDay");
}
if (myOptions.HasFlag(ShowOptions.Holiday))
{
    Console.WriteLine("myOptions包含Holiday");
}

若要判斷是否必需同時包含Day與Night,則可以這樣寫:

 

myOptions = ShowOptions.Day | ShowOptions.Night;
if (myOptions.HasFlag(ShowOptions.Day | ShowOptions.Night))
{
    Console.WriteLine("myOptions包含Day與Night");
}

判斷是否包含A或B兩者其一,則可以這樣寫:

myOptions = ShowOptions.Day | ShowOptions.Night;
if (myOptions.HasFlag(ShowOptions.Day) || myOptions.HasFlag(ShowOptions.Night))
{
     Console.WriteLine("myOptions包含Day或Night");
}

 

若只允許接受特定列舉值,我們可以這樣寫;

myOptions = ShowOptions.Day | ShowOptions.Night;
ShowOptions allowOptions = ShowOptions.Holiday ;
Console.WriteLine((myOptions & allowOptions) != 0); //false
allowOptions = ShowOptions.Day;
Console.WriteLine((myOptions & allowOptions) != 0); //true

 

取得列舉中所有列舉值

若要取得列舉中所有列舉值,可以利用一段foreach迴圈來處理,參考以下程式碼:

foreach (var e in Enum.GetValues(typeof(ShowOptions)))
{
    Console.WriteLine($"{e} = {(int)e}");
}

執行後將印出以下資訊:

Day = 1
Night = 2
WeekDay = 4
Holiday = 16

 

加總列舉值

若使用者選取了Day、Night、WeekDay,可計算其加總:

var selectdValues = new[] { ShowOptions.Day, ShowOptions.Night, ShowOptions.WeekDay };
//等同於 var selectdValues = new[] { 1, 2, 4 };
var total = selectdValues.Aggregate(0, (current, v) => current | (int)v);
Console.WriteLine(total); //7

也可以使用LINQ,參考以下範例程式碼:

var total2 = selectdValues.Sum(x => (int)x);
Console.WriteLine(total2); //7

 

在MVC專案使用列舉與旗標

接下來讓我們說明在MVC專案使用列舉與旗標來設計多選的功能。假設目前有一個Movie模型描述電影的編號(MovieId)與電影名稱(Title),參考以下程式碼,其中的ShowOptions屬性的型別就是一個列舉旗標,描述影片撥放時段,可能是白天(Day)、晚上(Night)、平日(WeekDay)或假日(Holiday),可以有多選:

[Flags]
public enum ShowOptions
{
    Day = 1,
    Night = 2,
    WeekDay = 4,
    Holiday = 8
}

public class Movie {
  public int MovieId { get; set; }
  public string Title { get; set; }
  public ShowOptions ShowOptions { get; set; }
}

 

在專案中定義MovieContext類別如下,以便透過Entity Framework將資料寫到資料庫:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using EnumFlagMovieDemo.Models;

namespace EnumFlagMovieDemo.DAL {
    public class MovieContext : DbContext {
        public DbSet<Movie> Movies { get; set; }
    }
}


然後使用Visual Studio工具Scaffold功能自動產生利用Entity Framework讀寫資料的控制器與檢視程式碼,請參考下圖所示,新增一個控制器,從範本中選取「MVC 5 Controller with views, using Entity Framework」:

clip_image014

圖 7

下一步設定模型為「Movie」;Data context class為「MovieContext」,控制器的名稱為「MoviesConttoller」,請參考下圖所示,然後按下「Add」按鈕產生程式碼:

clip_image016

圖 8

工具產生的Create檢視中的程式碼,預設採用文字方塊來顯示ShowOptions,這樣不易使用者進行資料的輸入。讓我們修改產生出來的Create檢視,加入以下<ul>標籤,利用四個Checkbox讓使用者輸入ShowOptions:

@model EnumFlagMovieDemo.Models.Movie

@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Create</title>
</head>
<body>
  @using (Html.BeginForm()) {
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
      <h4>Movie</h4>
      <hr />
      @Html.ValidationSummary(true, "", new { @class = "text-danger" })
      <div class="form-group">
        @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
          @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        @Html.LabelFor(model => model.ShowOptions, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          <ul>
            <li>
              <input type="checkbox" name="showOptions" value="Day" id="showOptions1" />
              <label for="showOptions1">Day</label>
            </li>
            <li>
              <input type="checkbox" name="showOptions" value="Night" id="showOptions2" />
              <label for="showOptions2">Night</label>
            </li>
            <li>
              <input type="checkbox" name="showOptions" value="Weekday" id="showOptions3" />
              <label for="showOptions3">Weekday</label>
            </li>
            <li>
              <input type="checkbox" name="showOptions" value="Holiday" id="showOptions4" />
              <label for="showOptions4">Holiday</label>
            </li>
          </ul>
          @Html.ValidationMessageFor(model => model.ShowOptions, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <input type="submit" value="Create" class="btn btn-default" />
        </div>
      </div>
    </div>
  }

  <div>
    @Html.ActionLink("Back to List", "Index")
  </div>
</body>
</html>

因為MVC預設的模型繫結器(Model Binder)無法處理Flags類型的列舉值,因此我修改在MoviesController控制器標識HttpPost的Create方法,宣告一個ShowOptions[]型別的陣列參數來接資料,Create方法中計算出Enum Flag加總的值,填回Opera物件ShowOptions屬性,以更新到資料庫:

public ActionResult Create() {
  return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "MovieId,Title,ShowOptions")] Movie movie, ShowOptions[] showOptions) {
  if (ModelState.IsValid) {
    movie.ShowOptions = (ShowOptions)showOptions.Sum(i => (int)i);
    db.Movies.Add(movie);
    db.SaveChanges();
    return RedirectToAction("Index");
  }

  return View(movie);
}

 

執行Create檢視,在網頁中,便可以透過CheckBox來進行多選,請參考下圖所示:

clip_image018

圖 9

在檢視使用迴圈產生CheckBox

在檢視中寫死產生四個Checkbox程式碼的做法較不彈性,讓我改以Edit檢視來說明,以和Create檢視做個對照,修改Edit檢視利用程式根據列舉值的個數,在檢視使用迴圈來產生CheckBox,參考以下程式碼:

@model EnumFlagMovieDemo.Models.Movie
@using EnumFlagMovieDemo.Models
@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Edit</title>
</head>
<body>
  @using (Html.BeginForm()) {
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
      <h4>Movie</h4>
      <hr />
      @Html.ValidationSummary(true, "", new { @class = "text-danger" })
      @Html.HiddenFor(model => model.MovieId)

      <div class="form-group">
        @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
          @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        @Html.LabelFor(model => model.ShowOptions, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          <div>
            @foreach (ShowOptions ops in Enum.GetValues(typeof(ShowOptions))) {
              <input type="checkbox"
                     @(Model.ShowOptions.HasFlag(ops) ? "checked" : string.Empty)
                     name="showOptions" value="@ops" />
              @Html.Label("showOptions", ops.ToString())
            }
          </div>
          @Html.ValidationMessageFor(model => model.ShowOptions, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <input type="submit" value="Save" class="btn btn-default" />
        </div>
      </div>
    </div>
  }

  <div>
    @Html.ActionLink("Back to List", "Index")
  </div>
</body>
</html>

 

同樣地,需要修改MoviesController控制器的程式碼,讓標識HttpPost的Edit方法,宣告一個ShowOptions[]型別的陣列參數來接資料,並在Edit方法中計算出Flag加總的值,填回Opera物件ShowOptions屬性,以更新到資料庫:

public ActionResult Edit(int? id) {
  if (id == null) {
    return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  }
  Movie movie = db.Movies.Find(id);
  if (movie == null) {
    return HttpNotFound();
  }
  return View(movie);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "MovieId,Title,ShowOptions")] Movie movie, ShowOptions[] showOptions) {
  if (ModelState.IsValid) {
   movie.ShowOptions = (ShowOptions)showOptions.Sum(i => (int)i);
    db.Entry(movie).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(movie);
}

 

執行後編輯畫面參考如下:

clip_image020

圖 10

若使用者執行階段選取Day與Night項目,foreach這段迴圈程式產生的標籤將會如下:

<div>
<input type="checkbox" checked name="showOptions" value="Day">
<label for="showOptions">Day</label>
<input type="checkbox" checked name="showOptions" value="Night">
<label for="showOptions">Night</label>
<input type="checkbox" name="showOptions" value="WeekDay">
<label for="showOptions">WeekDay</label>
<input type="checkbox" name="showOptions" value="Holiday">
<label for="showOptions">Holiday</label>
</div>

 

自訂模型繫結器

由於預設的模型繫結器不支援列舉旗標,因此較好的做法可以自訂模型繫結器(Model Binder)來處理繫結的問題,這樣就不必在控制器方法的參數額外宣告一個ShowOptions[]型別的參數來接列舉值。以編輯為例,修改標識HttpPost的Edit方法,回到工具產生的程式碼:

public ActionResult Edit(int? id) {
   if (id == null) {
     return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
   }
   Movie movie = db.Movies.Find(id);
   if (movie == null) {
     return HttpNotFound();
   }
   return View(movie);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "MovieId,Title,ShowOptions")] Movie movie) {
   if (ModelState.IsValid) {
     db.Entry(movie).State = EntityState.Modified;
     db.SaveChanges();
     return RedirectToAction("Index");
   }
   return View(movie);
}

 

在專案中加入一個MyFlagEnumModelBinder類別,繼承DefaultModelBinder,自訂模型繫結器,然後改寫BindModel()方法,在方法中從ValueProvider取得RawValue,它存放使用者選取的列舉項目,如Day、Night,然後利用Activator.CreateInstance動態建立列舉型別,再透過Enum.Parse將字串轉成列舉值回傳:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;

namespace EnumFlagMovieDemo {
  public class MyFlagEnumModelBinder : DefaultModelBinder {
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
      var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
      Type type = bindingContext.ModelType;
      if (value != null) {
        var rawValue = value.RawValue as string[];
        if (rawValue != null) {
          var result = (Enum)Activator.CreateInstance(type);
          try {
            result = (Enum)Enum.Parse(type, string.Join(",", rawValue));
            return result;
          } catch {
            return base.BindModel(controllerContext, bindingContext);
          }
        }
      }
      return base.BindModel(controllerContext, bindingContext);
    }
  }
}

 

最後在Global.asax註冊自訂的MyFlagEnumModelBinder型別:

using EnumFlagMovieDemo.DAL;
using EnumFlagMovieDemo.Models;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace EnumFlagMovieDemo {
  public class MvcApplication : System.Web.HttpApplication {
    protected void Application_Start() {
      AreaRegistration.RegisterAllAreas();
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      Database.SetInitializer(new MoviesInitializer());
      ModelBinders.Binders.Add(typeof(ShowOptions), new MyFlagEnumModelBinder());
    }
  }
}


執行測試將會得到和上一節一樣的結果。

 

自訂HTML Helper

最後,為了方便使用,讓我們將產生標籤的動作設計成HTML Helper,產生適用於套用Bootstrap樣式的標籤來顯示列舉資料。此外,預設CheckBox會在網頁中顯示列舉值,如Day、Night,但實際上想顯示在網頁中的文字可能不相同,我們可以一併用HTML Helper,搭配Display Attribute來處理這個問題。首先修改ShowOptions定義,讓每一個列舉值上方套用Display Attribute,設定想要呈現在檢視中的文字:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace EnumFlagMovieDemo.Models {

  [Flags]
  public enum ShowOptions {
    [Display(Name = "白天")]
    Day = 1,
    [Display(Name = "晚上")]
    Night = 2,
    [Display(Name = "平日")]
    WeekDay = 4,
    [Display(Name = "假日")]
    Holiday = 8
  }

  public class Movie {
    [Display(Name = "編號")]
    public int MovieId { get; set; }
    [Display(Name = "名稱")]
    public string Title { get; set; }
    [Display(Name = "撥放時段")]
    public ShowOptions ShowOptions { get; set; }
  }
}

 

在專案中加入一個MyFlagEnumHelper靜態類別,在類別中加入一個MyEnumCheckBox擴充方法,用來加入產生標籤的邏輯:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace EnumFlagMovieDemo {
  public static class MyFlagEnumHelper {
    public static IHtmlString MyEnumCheckBox<TModel, TValue>(this HtmlHelper<TModel> html,
      Expression<Func<TModel, TValue>> expression, object htmlAttributes = null) {

      var field = ExpressionHelper.GetExpressionText(expression);
      var fieldName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(field);
      var selectedValues = ModelMetadata.FromLambdaExpression(expression, html.ViewData).Model;

      IEnumerable<TValue> enumValues = Enum.GetValues(typeof(TValue)).Cast<TValue>();

      StringBuilder sb = new StringBuilder();
      foreach (var item in enumValues) {
        TagBuilder labelBuilder = new TagBuilder("label");
        if (htmlAttributes != null) {
          var attr = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
          labelBuilder.MergeAttributes(attr);
        }
        TagBuilder inputBuilder = new TagBuilder("input");
        long enumValue = Convert.ToInt64(item);
        long value = Convert.ToInt64(selectedValues);

        if ((value & enumValue) == enumValue) {
          inputBuilder.Attributes["checked"] = "checked";
        }
        inputBuilder.Attributes["type"] = "checkbox";
        inputBuilder.Attributes["value"] = item.ToString();
        inputBuilder.Attributes["name"] = fieldName;
        var attributes = (DisplayAttribute[])item.GetType().GetField(
            Convert.ToString(item)).GetCustomAttributes(typeof(DisplayAttribute), false);
        labelBuilder.InnerHtml = inputBuilder + (attributes.Length > 0 ? attributes[0].Name : Convert.ToString(item));
        sb.Append(labelBuilder.ToString());
      }
      return new MvcHtmlString(sb.ToString());
    }
  }
}

 

後續只要修改Edit檢視,叫用Html.MyEnumCheckBox來產生標籤:

@model EnumFlagMovieDemo.Models.Movie
@using EnumFlagMovieDemo.Models
@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Edit</title>
</head>
<body>
  @using (Html.BeginForm()) {
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
      <h4>Movie</h4>
      <hr />
      @Html.ValidationSummary(true, "", new { @class = "text-danger" })
      @Html.HiddenFor(model => model.MovieId)

      <div class="form-group">
        @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
          @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        @Html.LabelFor(model => model.ShowOptions, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          <div>
            @Html.MyEnumCheckBox(model => model.ShowOptions,htmlAttributes: new { @class = "checkbox-inline" })
          </div>
          @Html.ValidationMessageFor(model => model.ShowOptions, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <input type="submit" value="Save" class="btn btn-default" />
        </div>
      </div>
    </div>
  }

  <div>
    @Html.ActionLink("Back to List", "Index")
  </div>
</body>
</html>

 

Edit檢視最後的執行畫面參考如下:

clip_image022

圖 11

產生出的HTML標籤適用於Bootstrap,參考以下程式碼:

<label class="checkbox-inline">
  <input name="ShowOptions" type="checkbox" value="Day">
  白天
</label>
<label class="checkbox-inline">
  <input checked="checked" name="ShowOptions" type="checkbox" value="Night">
  晚上
</label>
<label class="checkbox-inline">
  <input name="ShowOptions" type="checkbox" value="WeekDay">
  平日
</label>
<label class="checkbox-inline">
  <input checked="checked" name="ShowOptions" type="checkbox" value="Holiday">
  假日
</label>

使用MVC整合Bootstrap對話盒新增資料 - 1

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N170718501
出刊日期: 2017/7/12

本文將介紹如何在MVC 5的專案之中,整合Bootstrap的對話盒,設計資料新增功能,利用Entity Framework Code First技術自動建立資料庫,存取資料庫資料。如此不需要自己撰寫複雜的AJAX程式碼來更新網頁部分頁面。

 

建立範例專案

為了方便說明,我們先使用Visual Studio 2015來建立MVC 5的網站,從「File」-「New」-「Project」,在「New Project」對話盒中,確認視窗上方.NET Framework的目標版本為「.NET Framework 4.6」以上,選取左方「Installed」-「Templates」-「Visual C#」程式語言,從「Web」分類中,選取「ASP.NET Web Application(.NET Framework)」,適當設定專案名稱與專案存放路徑,按下「OK」鍵,請參考下圖所示:

clip_image002

圖 1:建立MVC 5專案。

在「New ASP.NET Web Application」對話盒中選取「MVC」,勾選下方的「MVC」項目,然後按一下畫面中的「OK」 按鈕,請參考下圖所示:

clip_image004

圖 2:勾選下方的「MVC」項目。

專案建立之後,可以從「Solution Explorer」視窗,檢視目前專案的結構,專案中會產生一個「Content」資料夾,裏頭存放Bootstrap套件相關的樣式表檔案,以及一個「Scripts」資料夾,其中存放jQuery與Bootstrap套件相關的JavaScript程式碼檔案,請參考下圖所示:

clip_image006

圖 3:範本專案的檔案結構。

更新Bootstrap版本到目前最新的3.3.7版,從Visual Studio 2015開發工具 -「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Manage Nuget Packages」,開啟對話盒,請參考下圖所示:

clip_image008

圖 4:使用Nuget Package Manager更新套件。

接著選取視窗上方的「Installed」選項,下方便會列出目前專案已安裝的套件,選取其中的bootstrap套件,在右方的下拉式清單方塊中,選取要使用的「3.3.7」版,然後按下「Update」按鈕進行更新,請參考下圖所示:

clip_image010

圖 5:更新套件到指定的版本。

Nuget工具會自動偵測套件的相依性,來安裝其它的相依套件,例如Bootstrap 3.3.7版要求相依的jQuery版本需要大於等於1.9.1版以上。

 

建立Employee模型

從Visual Studio 2015開發工具 -「Solution Explorer」- 專案 -「Models」目錄上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,開啟「Add New Item」對話盒,請參考下圖所示:

clip_image012

圖 6:加入新項目。

在「Add New Item」對話盒選取「Class」項目,設定檔案名稱為「Employee.cs」,請參考下圖所示:

clip_image014

圖 7:加入Employee.cs檔案。

在Employee類別定義以下屬性,並設定Attribute,其中Name屬性的上方套用Required Attribute,並設定姓名空白時要顯示的自訂錯誤訊息:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;

namespace DialogDemo.Models
{
    public class Employee
    {
        [Display(Name = "員工編號")]
        public int Id { get; set; }

        [Display(Name = "姓名")]
        [Required(ErrorMessage = "姓名不可以為空白")]
        [StringLength(200)]
        public string Name { get; set; }

        [Display(Name = "身高")]
        public int Height { get; set; }

        [Display(Name = "生日")]
        [DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}", ApplyFormatInEditMode = true)]
        public DateTime BirthDate { get; set; }

        [Display(Name = "婚姻狀態")]
        public bool Married { get; set; }
    }
}

 

 

建立ADO.NET實體資料模型

下一步是使用Visual Studio的功能來定義ADO.NET實體資料模型,然後利用Entity Framework Code First技術自動建立資料庫。在MVC的專案之中,通常將ADO.NET實體資料模型的相關程式置於Models資料夾之中。

從Visual Studio 2015開發工具 -「Solution Explorer」- 專案-「Models」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」,開啟「Add New Item」對話盒,請參考下圖所示:

clip_image015

圖 8:加入新項目。

在「Add New Item」對話盒選取「ADO.NET Entity Data Model」項目,設定名稱為「EmployeesContext」,請參考下圖所示:

clip_image017

圖 9:建立ADO.NET實體資料模型。

然後選取「Empty Code First model」項目,按下「Finish」按鈕,請參考下圖所示:

clip_image019

圖 10:選取「Empty Code First model」項目。

當精靈完成之後,接著Visual Studio就會自動在專案中Models資料夾內產生一個EmployeesContext.cs檔案,預設包含以下程式碼,為了簡單起見,底下列出的程式碼已經刪除程式碼註解部分的說明:

namespace DialogDemo.Models
{
    using System;
    using System.Data.Entity;
    using System.Linq;

    public class EmployeesContext : DbContext
    {
        public EmployeesContext()
            : base("name=EmployeesContext")
        {
        }
    }
}

 

此外這個步驟完成後,也會自動在專案根目錄下的web.config檔案中,設定資料庫連接字串,預設將資料庫建立在LocalDb之中):

<connectionStrings>
  <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\aspnet-DialogDemo-20170601035448.mdf;Initial Catalog=aspnet-DialogDemo-20170601035448;Integrated Security=True" providerName="System.Data.SqlClient" />
  <add name="EmployeesContext" connectionString="data source=(LocalDb)\MSSQLLocalDB;initial catalog=DialogDemo.Models.EmployeesContext;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>

修改EmployeesContext類別,為其加入一個Employees屬性,其型別是DbSet<Employee>:

namespace DialogDemo.Models
{
    using System;
    using System.Data.Entity;
    using System.Linq;

    public class EmployeesContext : DbContext
    {
        public EmployeesContext()
            : base("name=EmployeesContext")
        {
        }

        public DbSet<Employee> Employees { get; set; }
    }
}

選Visual Studio開發工具 - 「Build」- 「Build Solution」項目,確認專案目前可以正確編譯。從Visual Studio開發工具的「Tools」-「Library Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入以下指令,利用ContextTypeName參數指定EmployeesContext類別的完整名稱,啟用Code First Migration功能:

Enable-Migrations -ContextTypeName DialogDemo.Models.EmployeesContext

得到的執行結果如下圖所示:

clip_image021

圖 11:圖 12:選取「Empty Code First model」項目。

修改程式碼如下,利用程式碼新增幾筆測試資料到資料庫資料表中:

namespace DialogDemo.Migrations
{
    using Models;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<DialogDemo.Models.EmployeesContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
        }

        protected override void Seed(DialogDemo.Models.EmployeesContext context)
        {
            var employees = new List<Employee>{
                new Employee {
                    Id = 1,
                    Name = "Mary",
                    Height  = 160,
                    BirthDate = new DateTime(1951,1,3),
                    Married = false
                },
                new Employee {
                    Id = 2,
                    Name = "Candy",
                    Height  = 160,
                    BirthDate = new DateTime(1945,3,4),
                    Married = true
                },
                new Employee {
                    Id = 3,
                    Name = "Judy",
                    Height  = 172,
                    BirthDate = new DateTime(1978,8,3),
                    Married = true
                },
                new Employee {
                    Id = 4,
                    Name = "Bob",
                    Height  = 184,
                    BirthDate = new DateTime(1951,11,3),
                    Married = false
                },
                new Employee {
                    Id = 5,
                    Name = "Jeffery",
                    Height  = 158,
                    BirthDate = new DateTime(1964,5,23),
                    Married = false
                },
                new Employee {
                    Id = 6,
                    Name = "Sunny",
                    Height  = 165,
                    BirthDate = new DateTime(1988,10,2),
                    Married = false
                }
            };

            employees.ForEach(s => context.Employees.Add(s));
        }
    }
}

選Visual Studio開發工具 - 「Build」- 「Build Solution」項目,確認專案目前可以正確編譯。

在「Package Manager Console」視窗提示字元中輸入以下指令,建立資料庫:

Update-Database

執行結果,請參考下圖所示:

clip_image023

圖 13:建立資料庫。

完成此步驟之後,會根據組態檔的設定,建立資料庫。

設計控制器

從Visual Studio 2015開發工具-「Solution Explorer」視窗- 專案 -「Controllers」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「Controller」,開啟「Add Scaffold」對話盒,請參考下圖所示:

clip_image025圖 14:新增控制器。

在「Add Scaffold」對話盒中選取「MVC 5 Controller with views, using Entity Framework」項目,然後按下「Add 」按鈕,請參考下圖所示:

clip_image027

圖 15:新增「MVC 5 Controller with views, using Entity Framework」項目。

在「Add Controller」對話盒設定以下項目:

  • a. Model class:選取「Employee」類別。
  • b. Data context class選取:「EmployeesContext」。
  • c. 勾選「Generate views」核取方塊。
  • d. 勾選「Reference script libraries」核取方塊。
  • e. 勾選「Use a layout page:」核取方塊。
  • f. 設定Controller名稱:「EmployeesController」。

請參考下圖所示:

clip_image029

圖 16:新增控制器。

然後按下「Add」按鈕。Visual Studio 2015便會在「Views\Employees」資料夾下,新增檢視檔案,以及在「Controllers」資料夾下,新增EmployeesController.cs,包含控制器程式碼。

選取Visual Studio開發工具,「Build」-「Build Solution」編譯目前的專案,確認程式碼能正確編譯。

從「Solution Explorer」視窗- 選取Views\Employees資料夾下的Index.cshtml 檔案,按CTRL+F5執行Index檢視,現在你可以使用工具產生出的程式碼編修資料,請參考下圖所示:

clip_image031

圖 17:查詢資料表資料。

目前若點選「Create」超連結,會跳到另一個畫面輸入資料,請參考下圖所示:

clip_image033

圖 18:新增資料表資料。

設計資料新增功能

接著我們將改用Bootstrap對話盒來設計資料新增功能。首先改寫Controllers\EmployeesController.cs檔案中的EmployeesController類別,新增兩個「_Create」方法,方法的名稱故意以「_」開始和Visuaul Studio工具產生的「Create」方法有所區別。這兩個「_Create」方法叫用PartialView()方法,回傳部分檢視的內容。此外我們不希望使用者直接在瀏覽器直接輸入URL:「http://localhost /Employees/_Create」來叫用此方法,所以Create方法上方套用了[ChildActionOnly] Attribute:

[ChildActionOnly]
public ActionResult _Create() {
  return PartialView();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult _Create([Bind(Include = "Id,Name,Height,BirthDate,Married")] Employee employee) {
  if (ModelState.IsValid) {
    db.Employees.Add(employee);
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return PartialView(employee);
}


建立_Create.cshtml檔案,從「Solution Explorer」視窗「Views\Employees」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」選項,選取「View 」,請參考下圖所示:

clip_image035

圖 19:加入_Create檢視。

在下一個畫面,將檔案名稱設定為「_Create.cshtml」,其它項目保留預設設定,然後按下「Add」按鈕,,請參考下圖所示:

clip_image037

圖 20:建立_Create 檢視。

下一步驟是修改新建立的Views\Employees\_Create.cshtml檔案,套用Bootstrap的對話盒與標籤:

@model DialogDemo.Models.Employee
@{
  ViewBag.Title = "Create";
}
<!-- Modal -->
@using (Html.BeginForm("_Create", "Employees")) {
  @Html.AntiForgeryToken()

  <div id="myModal" class="modal fade" role="dialog">
    <div class="modal-dialog">

      <!-- Modal content-->
      <div class="modal-content">
        <div class="modal-header modal-header-primary">
          <button type="button" class="close" aria-label="Close" data-dismiss="modal">&times;</button>
          <h4 class="modal-title">Employee新增</h4>
        </div>
        <div class="modal-body">
          <div class="form-horizontal">
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            <div class="form-group">
              @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
              <div class="col-md-10">
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
              </div>
            </div>

            <div class="form-group">
              @Html.LabelFor(model => model.Height, htmlAttributes: new { @class = "control-label col-md-2" })
              <div class="col-md-10">
                @Html.EditorFor(model => model.Height, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Height, "", new { @class = "text-danger" })
              </div>
            </div>

            <div class="form-group">
              @Html.LabelFor(model => model.BirthDate, htmlAttributes: new { @class = "control-label col-md-2" })
              <div class="col-md-10">
                @Html.EditorFor(model => model.BirthDate, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.BirthDate, "", new { @class = "text-danger" })
              </div>
            </div>

            <div class="form-group">
              @Html.LabelFor(model => model.Married, htmlAttributes: new { @class = "control-label col-md-2" })
              <div class="col-md-10">
                <div class="checkbox">
                  @Html.EditorFor(model => model.Married)
                  @Html.ValidationMessageFor(model => model.Married, "", new { @class = "text-danger" })
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="modal-footer modal-footer-primary">
          <button type="submit" class="btn btn-default">Create</button>
        </div>
      </div>
    </div>
  </div>
}

修改Views\Employees\Index.cshtml檔案,在檔案最下方,加入以下程式碼,利用Html.Action插入

@model IEnumerable<DialogDemo.Models.Employee>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Height)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.BirthDate)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Married)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Height)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.BirthDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Married)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
            @Html.ActionLink("Details", "Details", new { id=item.Id }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.Id })
        </td>
    </tr>
}

</table>

<p>
    <button type="button" class="btn btn-info btn-lg" data-toggle="modal" data-target="#myModal">新增</button>
</p>
@Html.Action("_Create")

 

資料新增時,需要利用jQuery Validation Plugin來進行資料驗證,現在新增的畫面內嵌在Index檢視之中,Index檢視又套用_Layout檔案,所以我們接著修改Views\Shared\_Layout.cshtml檔案,在「Scripts.Render("~/bundles/jquery")」這行程式碼下一行,引用jQuery Validation:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")

</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Home", "Index", "Home")</li>
                    <li>@Html.ActionLink("About", "About", "Home")</li>
                    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                </ul>
                @Html.Partial("_LoginPartial")
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/jqueryval")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

 

這樣所有的步驟就完成了,選取Visual Studio 「Build」-「Build Solution」編譯目前的專案,確認程式碼能正確編譯。從「Solution Explorer」視窗- 選取Views\Employees資料夾下的Index.cshtml 檔案,按CTRL+F5執行Index檢視,現在你可以使用工具產生出的程式碼編修資料,請參考下圖所示:

clip_image039

圖 21:範例測試。

當你點選畫面中的「新增」按鈕,就會看到一個空白的新增表單,以對話盒方式呈現,請參考下圖所示:

clip_image041

圖 22:新增資料測試

若輸入正確資料按下畫面中的「Create」按鈕,資料將寫回資料庫,Index檢視也會馬上呈現新增的資料,請參考下圖所示:

clip_image043

圖 23:資料成功新增,回到Index檢視。

若資料輸入有誤,則利用jQuery Validation進行驗證,並顯示錯誤的訊息,請參考下圖所示:

clip_image045

圖 24:資料有誤,顯示錯誤訊息。

在_Layout檔案加上CSS自訂Bootstrap對話盒的表頭與表尾樣式:

<style>
    .modal-header-primary {
        color: #fff;
        padding: 9px 15px;
        border-bottom: 1px solid #eee;
        background-color: #428bca;
        border-top-left-radius: 5px;
        border-top-right-radius: 5px;
    }

    .modal-footer-primary {
        color: #fff;
        padding: 9px 15px;
        border-top: 1px solid #eee;
        background-color: #428bca;
        border-bottom-left-radius: 5px;
        border-bottom-right-radius: 5px;
    }

</style>

 

套用樣式的執行結果,請參考下圖所示:

clip_image047

圖 25:自訂Bootstrap對話盒的表頭與表尾樣式。

C# 7新功能概覽 - 1

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N170718502
出刊日期: 2017/7/26

C# 語言每一個新版本都提供許多新語法,讓程式撰寫的動作可以變得更簡潔,本文將介紹C# 7 新增的新語法,並利用一些範例來了解這些語法。

在方法參數列宣告out參數

C# 7新增一個新功能,可以在方法參數列直接宣告out參數,如此可以讓程式碼更容易閱讀,想要使用到out參數時便可以馬上宣告,不需另外撰寫一行變數宣告以接收out參數的值。

我們先看看在C# 6中out參數的宣告與使用,參考以下範例程式碼,叫用Rectangle方法計算出矩型面積,利用參數列上宣告的area out參數,將計算出的面積透過out參數回傳到呼叫端的area變數,你需要先寫一行宣告area的int型變數,再將它傳入Rectangle方法:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        int area;
        Rectangle(l, w, out area);
        Console.WriteLine($"{area}");

    }
    static void Rectangle(int len, int width, out int area)
    {
        area = len * width;
    }
}


 

C# 7可以讓out參數的語法更簡潔,允許你在參數列(Argument List)傳遞out參數時一併宣告型別,不需要額外撰寫一行宣告變數的程式碼,例如改寫以上範例程式碼如下:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        Rectangle(l, w, out int area);
        Console.WriteLine($"{area}");

    }
    static void Rectangle(int len, int width, out int area)
    {
        area = len * width;
    }
}

 

此外當你在參數列使用out關鍵字宣告參數值,它的有效範圍會提升到外部範圍(Scope),因此我們可以在叫用Rectangle()方法的下一行,使用到area變數。不僅如此,C# 7也可以在參數列宣告out參數時利用var關鍵字由編譯器自動推論變數型別,例如以下程式碼:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        Rectangle(l, w, out var area);
        Console.WriteLine($"{area}");

    }
    static void Rectangle(int len, int width, out int area)
    {
        area = len * width;
    }
}

 

而在撰寫程式的過程,Visual Studio 2017也會提供提示的功能,顯示參數型別資訊,請參考下圖所示:

clip_image002

圖 1:Visual Studio 2017也會提供提示的功能。

Tuple物件

Tuple是一個簡單的資料結構,可以包含多個資料元素(Data Element),最早.NET Framework 4版就新增了Tuple物件,Tuple物件是一個有序的資料結構,可以儲存異質物件,但此版本提供的功能不太方便使用,要存取Tuple中的項目是透過Item1、Item2、Item3...的屬性語法,因此程式很容易誤判,或者寫錯。Tuple中可以存放多個項目,因此可以將它當做方法的回傳值,這是讓方法回傳多個值的一種解決方案。

例如以下C# 6的範例程式碼,在Rectangle方法建立一個Tuple物件,將矩型的面積與周長計算出來之後,透過Tuple回傳到呼叫端,在Main方法則用Item1與Item2屬性分別取得面積與周長的值:

class Program
{
    static void Main(string[] args)
    {
        Tuple<int, int> result = Rectangle(40, 20);
        Console.WriteLine($"Area = {result.Item1}");
        Console.WriteLine($"Perimeter = {result.Item2}");
    }
    static Tuple<int, int> Rectangle(int len, int width)
    {
        //面積 = 長乘以寬
        int area = len * width;
        //周長 = (長+寬) * 2
        int perimeter = (len + width) * 2;
        return Tuple.Create(area, perimeter);
    }
}

 

這個範例的執行結果參考如下:

clip_image004

圖 2:使用Tuple範例執行結果。

對於呼叫端而言,我們需要去記憶Item1代表的是面積,Item2代表的是周長,但當項目多時,就很容易搞錯,若要得到更多關於此版本的物件用法,請參閱本站一文,網址為:「http://blogs.uuu.com.tw/Articles/post/2016/05/04/Tuple簡介.aspx

而C# 7 則將Tuple物件納入支援,在語法上則改得更為簡潔,以使用輕量型的Tuple資料結構來表達多個資料項目。不過,要使用Tuple之前,需要先在專案之中下載System.ValueTuple套件。

下載與安裝「System.ValueTuple」套件

使用Nuget套件管理員下載與安裝「System.ValueTuple」套件的步驟如下,從Visual Studio 2017開發工具「Solution Explorer」視窗選取專案名稱。再從「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入install-package指令:

Install-Package System.ValueTuple

得到的執行結果如下圖所示:

clip_image006

圖 3:載與安裝「System.ValueTuple」套件。

使用Tuple物件

在C# 7中可以直接使用 ( ) 符號,以逗號區隔成員,將一到多個項目指定給Tuple物件,例如以下範例程式碼,將矩型的面積「800」與周長「120」指定給rectangle變數(它是一個Tuple物件),之後則可以利用Item1、Item2欄位語法將其中的項目讀出:

class Program
{
    static void Main(string[] args)
    {
        var rectangle = (800, 120);
        Console.WriteLine($"Area = {rectangle.Item1}");
        Console.WriteLine($"Perimeter = {rectangle.Item2}");
    }
  
}

這個範例的執行結果參考如下:

clip_image008

圖 4:使用Tuple物件。

當然在Tuple的小括號中,可以使用運算式進行一些計算,將運算的結果傳入Tuple物件,參考以下範例程式碼,這個範例的執行結果和上例一樣。

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        var rectangle = (l * w, (l + w) * 2);
        Console.WriteLine($"Area = {rectangle.Item1}");
        Console.WriteLine($"Perimeter = {rectangle.Item2}");
    }

}

 

甚至在小括號中也可以呼叫方法將方法回傳值指派給Tuple物件,參考以下範例程式碼,這個範例的執行結果和上例一樣。

class Program
{
    static void Main(string[] args)
    {
        int GetArea(int len, int width)
        {
            return len * width;
        }
        int GetPerimeter(int len, int width)
        {
            return (len + width) * 2;
        }
        int l = 40;
        int w = 20;
        var rectangle = (GetArea(l, w), GetPerimeter(l, w));
        Console.WriteLine($"Area = {rectangle.Item1}");
        Console.WriteLine($"Perimeter = {rectangle.Item2}");
    }

}

 

設定Tuple項目名稱

使用C# 7建立Tuple物件時,可以為其中的項目指定一個有意義的名稱,這樣的好處是程式將會變得更容易閱讀,此外Visual Studio在開發過程中,也可以提供智慧型提示功能,來輔助程式碼的撰寫。例如以下範例程式碼,定義rectangle Tuple物件時,使用名稱設定它包含兩個欄位(field):area與perimeter,這樣要讀取它們的值時便可以使用你定義的名稱(如rectangle.area):

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        (int area, int perimeter) rectangle = (l * w, (l + w) * 2);
        Console.WriteLine($"Area = {rectangle.area}");
        Console.WriteLine($"Perimeter = {rectangle.perimeter }");
    }

}

 

這些名稱只有在編譯階段存在,因此後續若透過反射(Reflection)機制在執行時期載入程式執行時,無法使用到這些名稱。另外在撰寫程式的過程中,Visual Studio將提供智慧型提示功能,避免程式寫錯:

clip_image010

圖 5:Visual Studio提供智慧型提示功能。

還有另一種為Tuple物件內含項目命名的寫法,在指派運算子(=)符號的右方,使用 ( ) 號設定Tuple項目時,直接搭配(:)號為其中的欄位取一個文字名稱,例如以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        var rectangle = (area: l * w, perimeter: (l + w) * 2);
        Console.WriteLine($"Area = {rectangle.area}");
        Console.WriteLine($"Perimeter = {rectangle.perimeter}");
    }

}

 

 

若指派運算子(=)符號的左方與右方都同時取了名稱,此時右方的名稱將會被忽略,而以左邊設定的名稱為準,例如以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        (int Area, int Perimeter) rectangle = (area: l * w, perimeter: (l + w) * 2);
        Console.WriteLine($"Area = {rectangle.Area}");
        Console.WriteLine($"Perimeter = {rectangle.Perimeter}");
    }

}


 

在方法中使用Tuple物件回傳值

以往在C# 6,方法只能回傳一個值,或要回傳多個值,需要透過回傳陣列、集合或自訂結構(Struct)自訂類別(Class),再或者利用前述的out參數、ref參數來達成。而在C# 7使用Tuple物件則可以很容易的從方法回傳多個值,而不需要再透過陣列、集合,或者自訂的結構(Struct)或自訂類別(Class)類別來回傳多個值。例如以下的範例程式碼,讓Rectangle方法回傳一個Tuple物件,其中包含兩個欄位記錄矩型的面積與周長的值。

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        var rectangle = Rectangle(l, w);
        Console.WriteLine($"Area = {rectangle.Area}");
        Console.WriteLine($"Perimeter = {rectangle.Perimeter}");
    }
    static (int Area, int Perimeter) Rectangle(int len, int width)
    {
        return (len * width, (len + width) * 2);
    }
}

 

若Rectangle方法回傳型別定義為「(int, int)」,呼叫端就必須使用Item1、Item2語法來讀它的值,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        var rectangle = Rectangle(l, w);
        Console.WriteLine($"Area = {rectangle.Item1}");
        Console.WriteLine($"Perimeter = {rectangle.Item2}");
    }
    static (int, int) Rectangle(int len, int width)
    {
        return (len * width, (len + width) * 2);
    }
}

 

而以下在建立Tuple物件的 ( ) 號中設定名稱的寫法是無用的,因為設定的名稱會被方法回傳型別的「(int, int)」覆蓋,呼叫端還是得使用Item1、Item2語法來讀它的值:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        var rectangle = Rectangle(l, w);
        Console.WriteLine($"Area = {rectangle.Item1}");
        Console.WriteLine($"Perimeter = {rectangle.Item2}");
    }
    static (int, int) Rectangle(int len, int width)
    {
        return (area: len * width, perimeter: (len + width) * 2);
    }
}

 

Deconstructing

有時我們需要將方法回傳的值指定給變數,若方法回傳Tuple物件,你可以利用Deconstructing語法,將Tuple物件中的多個欄位一次指派給多個變數。例如以下範例程式碼,將Rectangle方法回傳的Tuple物件之第一個欄位值指派給area變數;Tuple物件之第二個欄位值指派給perimeter變數:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        (int area, int perimeter) = Rectangle(l, w);
        Console.WriteLine($"Area = {area}");
        Console.WriteLine($"Perimeter = {perimeter}");
    }
    static (int Area, int Perimeter) Rectangle(int len, int width)
    {
        return (len * width, (len + width) * 2);
    }
}

 

若自訂類別也想要提供Deconstructing功能,可以在類別中定義一個Deconstruct()方法,此方法利用一到多個out參數將運算後的值回傳。參考以下範例程式碼,Rectangle類別提供Deconstruct()方法將Area與Perimeter屬性的值粹取出來,放到呼叫端Main方法中的變數 area、perimeter:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        Rectangle r = new Rectangle(l, w);
        (int area, int perimeter) = r;
        Console.WriteLine($"Area = {area}");
        Console.WriteLine($"Perimeter = {perimeter}");
    }
}
public class Rectangle
{
    public Rectangle(int len, int width)
    {
        this.Area = len * width;
        this.Perimeter = (len + width) * 2;
    }
    public void Deconstruct(out int area, out int perimeter)
    {
        area = this.Area;
        perimeter = this.Perimeter;
    }
    public int Area { get; set; }
    public int Perimeter { get; set; }
}

 

也可以改寫如下,將建立(new)物件與解構賦值的程式寫在同一行:

class Program
{
    static void Main(string[] args)
    {
        int l = 40;
        int w = 20;
        (int area, int perimeter) = new Rectangle(l, w);
        Console.WriteLine($"Area = {area}");
        Console.WriteLine($"Perimeter = {perimeter}");
    }
}
public class Rectangle
{
    public Rectangle(int len, int width)
    {
        this.Area = len * width;
        this.Perimeter = (len + width) * 2;
    }
    public void Deconstruct(out int area, out int perimeter)
    {
        area = this.Area;
        perimeter = this.Perimeter;
    }
    public int Area { get; set; }
    public int Perimeter { get; set; }
}

C# 7新功能概覽 - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N170818601
出刊日期: 2017/8/9

本文將延續本站《C#7新功能概覽 - 1》一文的說明,介紹C# 7 新增的新語法,並利用一些範例來了解這些語法。

Pattern matching

C# 7的Pattern matching可以根據特定的類別或是結構來進行比對,看看比對的結果是否符合特定的樣式。Pattern matching功能支援兩個運算式(Expression):is與switch。這些運算式可以檢視物件與物件的屬性,來判斷物件是否滿足某種樣式(Pattern),還可以搭配when關鍵字以指定樣式的相關規則。

is樣式運算式(Pattern Expression

is樣式運算式(Pattern Expression)擴充原有的is運算子(Operator)的功能,以根據型別來查詢物件。

例如以下範例程式碼,在集合中存放數值與包含NT$貨幣符號的字串,我們希望將計算出所有項目的加總,若集合中存放的是數值,直接累加到total,若集合中存放的是帶貨幣符號的字串,則先將字串根據台灣文特性來轉型,粹取出數值,再和total進行累加,在C# 6可以撰寫這樣的程式碼來達到這個需求:

class Program
{
    static void Main(string[] args)
    {
        int x = 100;
        string y = "NT$200";
        IEnumerable<object> data = new List<object>() { x, y };
        int total = 0;

        foreach (var item in data)
        {
            if (item is int)
            {
                total += (int)item;
            }
            else if (item is string && item.ToString().Contains("NT$"))
            {
                total += int.Parse(item.ToString(), NumberStyles.Currency, new CultureInfo("zh-TW"));
            }
        }

        Console.WriteLine(total);
    }

}

 

此範例的執行結果會印出累計的結果「300」,而在C# 7可以改寫如下:

class PatternMatchingDemo
{
    static void Main(string[] args)
    {
        int x = 100;
        string y = "NT$200";
        IEnumerable<object> data = new List<object>() { x, y };
        int total = 0;

        foreach (var item in data)
        {
            if (item is int i)
            {
                total += i;
            }
            else if (item is string s && s.Contains("NT$"))
            {
                total += int.Parse(item.ToString(), NumberStyles.Currency, new CultureInfo("zh-TW"));
            }
        }

        Console.WriteLine(total);
    }
}

 

比對程式碼,在C# 7 if判斷式中,使用is比對集合中的項目的型別若為「int」,可以直接將item指定一個變數名稱「i」,在if的區塊中使用它。而第二個if 則利用一行程式碼,完成兩個動作,首先在判斷集合中的項目是否是「string」型別,若是字串型別,則自動將其轉型成字串,並指定給s變數。因此不需要像C# 6 還需要明確叫用ToString()方法將物件轉型成字串。

若集合中項目存放的是「null」時,要將total做累加一動作,例如以下將z變數放到集合中,此時就可以利用is運算子進行判斷:

class PatternMatchingDemo
{
    static void Main(string[] args)
    {
        int x = 100;
        string y = "NT$200";
        int? z = null;
        IEnumerable<object> data = new List<object>() { x, y, z };
        int total = 0;

        foreach (var item in data)
        {
            if (item is null)
            {
                total += 1;
            }
            else if (item is int i)
            {
                total += i;
            }
            else if (item is string s && s.Contains("NT$"))
            {
                total += int.Parse(item.ToString(), NumberStyles.Currency, new CultureInfo("zh-TW"));
            }
        }

        Console.WriteLine(total);
    }
}

 

switch式運算式(Pattern Expression)

若要判斷的條件有很多,不同的條件要分支去執行不同的程式碼,使用switch語法會比if語法看起來更為簡潔。在新的C# 7 switch語法,有著顯著的改善,可以撰寫類似VB的Select Case語法,更容易判斷資料是否在一個範圍,或符合特定條件。

以往在C# 6時,當運算分支的邏輯不是只拿一個簡單的型別做比對時,只能使用if - else if語法,現在C# 7使用新的Pattern matching語法可以讓程式更容易達到這樣的要求。switch陳述式與 case關鍵字之後的運算式現在不限定在只能使用數值(如int),或字串型別,可以包含變數或運算式,也可以使用到.NET型別或自訂類別。若在case關鍵字之後使用到「null」,則「null」將視為一個常數運算式(constant expresion),可以匹配null參考型別物件(null reference-type object)。

參考以下範例程式碼,從主控台輸入一個課程的成積,並透過ParseNullable方法,將其轉型成數值印出:

class Program
{
    public static int? ParseNullable(string val)
    {
        int output;
        return int.TryParse(val, out output) ? (int?)output : null;
    }
    static void Main(string[] args)
    {
        Console.WriteLine("請輸入一個數字");
        var scores = ParseNullable(Console.ReadLine());

        Console.WriteLine("中文");
        switch (scores)
        {
            case 100:
                Console.WriteLine("滿分");
                break;
            case var s when (s == 0):
                Console.WriteLine("零分");
                break;
            case var s when (s >= 99 & s <= 60):
                Console.WriteLine("及格");
                break;
            case var s when (s < 60 & s > 0):
                Console.WriteLine("不及格");
                break;
            default:
                Console.WriteLine("不正確的成績");
                break;
        }

    }
}

 

在第一個case區塊中,將數值與常數「100」做比對,而第二個case則在後面的運算式中,使用when關鍵字加上判斷條件,在成績(scores)為「0」時印出「零分」。第三個case則判斷成績是否在 99~60分之間;最後一個是預設值,因此若輸入負數和英文文字,如「a」,則會印出「不正確的成績」。

若輸入負數和英文文字要執行不同的程式邏輯,我們可以在switch的case區段判斷null值,

參考以下範例程式碼:

class Program
{
    public static int? ParseNullable(string val)
    {
        int output;
        return int.TryParse(val, out output) ? (int?)output : null;
    }
    static void Main(string[] args)
    {
        Console.WriteLine("請輸入一個數字");
        var scores = ParseNullable(Console.ReadLine());

        Console.WriteLine("中文");
        switch (scores)
        {
            case 100:
                Console.WriteLine("滿分");
                break;
            case var s when (s == 0):
                Console.WriteLine("零分");
                break;
            case var s when (s >= 99 & s <= 60):
                Console.WriteLine("及格");
                break;
            case var s when (s < 60 & s > 0):
                Console.WriteLine("不及格");
                break;
            case var s when s is null:
                Console.WriteLine("null");
                break;
            default:
                Console.WriteLine("不正確的成績");
                break;
        }

    }
}

 

此範例若輸入負數印出「不正確的成績」,若輸入英文文字,根據ParseNullable方法的邏輯,若轉型失敗ParseNullable方法會回傳null,因此switch程式區段最後會印出「null」。

特別注意,在switch使用Pattern Expression時,case出現的順序會影響程式的執行,規則比較嚴格或特殊的case區塊,程式的位置要出現在比較上面。我們將前面範例改簡單一些,參考以下範例程式碼:

class Program
{
    public static int? ParseNullable(string val)
    {
        return int.TryParse(val, out int output) ? (int?)output : null;
    }
    static void Main(string[] args)
    {
        Console.WriteLine("請輸入一個數字");
        var scores = ParseNullable(Console.ReadLine());

        Console.WriteLine("中文");
        switch (scores)
        {
            case 100:
                Console.WriteLine("滿分");
                break;
            case var s when (s > 0):
                Console.WriteLine(s);
                break;
            default:
                Console.WriteLine("不正確的成績");
                break;
        }

    }
}

 

若輸入100則印出「滿分」;若輸入非100的正數值,就印出此數值,但若修改程式碼順序如下:

class Program
{
    public static int? ParseNullable(string val)
    {
        return int.TryParse(val, out int output) ? (int?)output : null;
    }
    static void Main(string[] args)
    {
        Console.WriteLine("請輸入一個數字");
        var scores = ParseNullable(Console.ReadLine());

        Console.WriteLine("中文");
        switch (scores)
        {
            case var s when (s > 0):
                Console.WriteLine(s);
                break;
            case 100:
                Console.WriteLine("滿分");
                break;
            default:
                Console.WriteLine("不正確的成績");
                break;
        }

    }
}

 

因為第二個case涵蓋在第一個case的範圍內,因此Visual Studio會顯示一個錯誤,無法編譯程式碼:

clip_image002

圖 1:switch case出現的順序會影響程式的編譯。

比對型別(Type )

在每一個「case」後的運算式(Expression)可以包含一個型別(Type)名稱再加一個變數名稱,根據型別來比對是否滿足條件,此變數的有效範圍限定在此case區塊之中。Null實體永遠不會匹配這些型別判斷的case區塊,若要判斷是否為Null值,直接使用一個「null」 實體即可,參考以下範例程式碼:

class PatternMatchingDemo
{
     static void Main(string[] args)
     {
         Employee employee1 = new Employee() { Name = "Mary" };
         Employee employee2 = new Sales() { Name = "Candy", Bonus = 1000 };
         Employee employee3 = new SalesLeader() { Name = "LuLu", Allowance = 5000 };

         var list = new List<Employee>() { employee1, employee2, employee3, null };

         foreach (var employee in list)
         {
             switch (employee)
             {
                 case SalesLeader l:
                     Console.WriteLine($"Employee Name {l.Name} , SalesLeader Allowance : {l.Allowance}");
                     break;
                 case Sales s:
                     Console.WriteLine($"Employee Name {s.Name} , Sales Bonus : {s.Bonus}");
                     break;
                 case null:
                     Console.WriteLine($"null");
                     break;
                 default:
                     Console.WriteLine($"Employee Name {employee.Name}");
                     break;
             }

         }

     }
}
class Employee
{
     public string Name { get; set; }
}

class Sales : Employee
{
     public int Bonus { get; set; }
}

class SalesLeader : Sales
{
     public int Allowance { get; set; }
}

 

SalesLeader類別繼承自Sales類別,而Sales類別繼承自Employee類別,範例中將SalesLeader、Sales與Employee物件放到集合之中,使用迴圈將集合中每一個物件取出,然後利用switch比對,若集合中的物件是SalesLeader類型的物件則印出Allowance;若集合中的物件是Sales類型則印出Bonus,此範例的執行結果,請參考下圖所示:

clip_image004

圖 2:型別比對。

進行型別比對(Type)時,case關鍵字後的運算式也可以搭配使用when關鍵字,例如將上例改寫如下,可以得到相同的執行結果:

 

class PatternMatchingDemo
{
     static void Main(string[] args)
     {
         Employee employee1 = new Employee() { Name = "Mary" };
         Employee employee2 = new Sales() { Name = "Candy", Bonus = 1000 };
         Employee employee3 = new SalesLeader() { Name = "LuLu", Allowance = 5000 };

         var list = new List<Employee>() { employee1, employee2, employee3, null };

         foreach (var employee in list)
         {
             switch (employee)
             {
                 case SalesLeader l:
                     Console.WriteLine($"Employee Name {l.Name} , SalesLeader Allowance : {l.Allowance}");
                     break;
                 case Sales s:
                     Console.WriteLine($"Employee Name {s.Name} , Sales Bonus : {s.Bonus}");
                     break;
                 case null:
                     Console.WriteLine($"null");
                     break;
                 default:
                     Console.WriteLine($"Employee Name {employee.Name}");
                     break;
             }

         }

     }
}
class Employee
{
     public string Name { get; set; }
}

class Sales : Employee
{
     public int Bonus { get; set; }
}

class SalesLeader : Sales
{
     public int Allowance { get; set; }
}


C# 7新功能概覽 - 3

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N170818602
出刊日期: 2017/8/23

本文將延續本站《C#7新功能概覽 - 1》、《C#7新功能概覽 - 2》文章的說明,介紹C# 7 新增的新語法,並利用一些範例來了解這些語法。

區域函式(Local Function)

使用過JavaScript程式語言的設計師一定相當熟悉在函式之中宣告函式,現在C# 也擁有類似的功能了。以一個範例來說明,在C# 6 除了匿名函式這種特殊案例之外,標準的方法只能在類別之中宣告,例如以下的SayHi方法:

class Program
{
  
    static void Main(string[] args)
    {
        Console.WriteLine(SayHi("mary"));
    }

    static string SayHi(string s)
    {
        return $"Hi, {s}";
    }
}

 

而在C# 7中,可以這樣改寫,在Main方法中直接宣告SayHi方法,這就叫區域函式(Local Function),參考以下範例程式碼:

class Program
{
     static void Main(string[] args)
     {
         string SayHi(string s)
         {
             return $"Hi, {s} ";
         }
         Console.WriteLine(SayHi("Mary"));
     }
}

區域函式(Local Function)可以直接存取它的外部函式宣告的變數值,例如以下範例程式碼,SayHi方法可以使用到外部Main方法宣告的today變數:

class Program
{
    static void Main(string[] args)
    {
        string today = DateTime.Now.ToShortDateString();

        string SayHi(string s)
        {
            return $"Hi, {s} , today is {today}";
        }
        Console.WriteLine(SayHi("Mary"));
    }
}

 

這個範例的執行結果參考如下:

 

clip_image002

圖 1:使用區域函式(Local Function)。

理所當然,在區域函式(Local Function)中定義的變數,有效範例隸屬區塊或函式等級,因此以下範例程式碼便無法在區域函式SayHi所在的外部函式Main方法讀取到SayHi中定義的name變數,而發生編譯錯誤:

class Program
{
    static void Main(string[] args)
    {
        string SayHi(string s)
        {
            var name = s;
            return $"Hi, {s} ";
        }

        Console.WriteLine(name); //Error
        Console.WriteLine(SayHi("Mary"));
    }
}

 

Expression-bodied Member

在C# 6定義類別的方法(Method)與屬性(Auto Property)時,可以直接使用Lambda Expression,讓程式又變短了,此語法稱之為Expression-bodied member,可以應用在定義方法(Expression-bodied method)與屬性(Expression-bodied property)。撰寫Expression-bodied method時,語法結構如下列範例的GetName()方法,在「=>」符號右方直接撰寫方法程式碼,若方法有回傳值,連return關鍵字都可以省略。而Expression-bodied property語法類似Expression-bodied method,如下範例中的FullName屬性,因為FullName後方沒有小括號,可以區別出它是屬性,方法名稱的後方需要有小括號。

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee();
        emp.LastName = "Lee";
        emp.FirstName = "Mary";
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

Expression-bodied constructor

C# 則擴充了Expression-bodied member,可以應用在建構函式(Constructor)、解構函式(Finalizer),以及含getter與setter的屬性語法。參考以下定義Expression-bodied constructor範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary");
        emp.LastName = "Lee";
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName) => FirstName = fName;

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

 

因為Expression-bodied member是用在只有一行程式碼的情境,若Employee的建構函式需要傳入兩個以上的參數,然後在方法中寫兩行程式碼來做初始設定,則Visual Studio將無法編譯程式碼。目前取代的作法是利用Tuple物件Deconstrunting的功能來初始化屬性值,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

但據說目前的Roslyn編譯器針對這樣的寫法,程式的效能不是很好,可能會再Visual Studio 2017 Update 1時有所修訂,改善其執行效能,可參閱以下的討論串:https://github.com/dotnet/roslyn/issues/16869#issuecomment-280800193

Expression-bodied Finalizer

解構函式也可以改用Expression-bodied Finalizer的寫法,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);
    //Expression-bodied Finalizer
    ~Employee() => Console.WriteLine("finalizer");
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);
    //Expression-bodied Finalizer
    ~Employee() => Console.WriteLine("finalizer");
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

Expression-bodied get / set accessor

在C# 6定義屬性時,可以利用get存取子,撰寫讀取屬性的程式碼;set存取子則用來撰寫設定資料的程式碼,例如以下的FirstName屬性:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
        Console.WriteLine(emp.FirstName); // Mary
    }
}
class Employee
{
    public Employee(string fName, string lName)
    {
        FirstName = fName;
        LastName = lName;
    }
    private string defaultFName;
    public string FirstName
    {
        get { return defaultFName; }
        set { defaultFName = value; }
    }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

在C# 7則可以改用Expression-bodied get / set accessor語法,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        Employee emp = new Employee("Mary", "Lee");
        Console.WriteLine(emp.GetName()); // Mary , Lee
        Console.WriteLine(emp.FullName); // Mary , Lee
        Console.WriteLine(emp.FirstName); // Mary

    }
}
class Employee
{
    //Expression-bodied constructor
    public Employee(string fName, string lName) => (FirstName, LastName) = (fName, lName);
    //Expression-bodied Finalizer
    ~Employee() => Console.WriteLine("finalizer");
    // Expression-bodied get / set accessors.
    private string defaultFName;
    public string FirstName {
        get => defaultFName;
        set => defaultFName = value;
    }
    public string LastName { get; set; }
    public string GetName() => FirstName + " , " + LastName; //Expression-bodied method
    public string FullName => this.FirstName + " , " + this.LastName; //Expression-bodied property
}

 

丟出例外

在C# 6 的throw屬於陳述式(Statement),不是運算式(Expression),而C# 陳述式(Statement)都是以分號結尾,例如「int i = 10;」而運算式(Expression)通常會計算出一個值,例如「1+2」。參考以下範例程式碼,若使用LINQ Find()方法查詢集合資料,找不到滿足的資料時,將回傳null值,在C# 6,我們需要另外再利用if程式碼判斷是否要丟出例外錯誤:

class Program
{
    static void Main(string[] args)
    {
        List<Employee> list = new List<Employee>(){
            new Employee { Name = "Mary" },
            new Employee { Name = "Candy"},
            new Employee { Name = "Amy" }
        };

        var r = list.Find(s => s.Name.StartsWith("a"));
        if (r == null)
        {
            throw new InvalidOperationException("Could not find data");
        }
    }
}
class Employee
{
    public string Name { get; set; }
}

 

 

而在C# 7 則可以改寫如下得到一樣的效果:

class Program
{
    static void Main(string[] args)
    {

        List<Employee> list = new List<Employee>(){
            new Employee { Name = "Mary" },
            new Employee { Name = "Candy"},
            new Employee { Name = "Amy" }
        };
        var r = list.Find(s => s.Name.StartsWith("a")) ?? throw new InvalidOperationException("Could not find data");
    }
}
class Employee
{
    public string Name { get; set; }
}

 

非同步方法回傳型別

在C# 6 一個非同步方法(async method)的回傳值只能是void、Task或Task<T>型別,若回傳Task或Task<T>,這兩種都是參考型別物件,因此記憶體的配置會比單純的實值型別(Value Type)來得多,這可能會造成效能上的瓶頸。參考以下範例程式碼,AddAsync非同步方法利用Task物件進行非同步的兩數相加的運算(非同步運算通常是應用在I/O或CPU密集的工作上,比較簡單的運算邏輯不需要使用到非同步,本範例純粹為了說明):

class Program
{
     static void Main(string[] args)
     {
         Task<int> task = AddAsync(10, 20);
         Console.WriteLine($"Result : {task.Result}");
     }
     static async Task<int> AddAsync(int x, int y)
     {
         var r = await Task.Run(() => x + y);
         return r;
     }
   
}

 

而C# 7的非同步方法現在則是允許你回傳一個較為輕量的實值型別,取代回傳一個參考型別物件,這樣可以有效的改善記憶體的使用。只要型別提供一個可存取的GetAwaiter()方法,就可以當作非同步方法的回傳型別。

不過要使用這個功能,需要先在專案中安裝「System.Threading.Tasks.Extensions 」套件,使用Nuget套件管理員下載與安裝「System.ValueTuple」套件的步驟如下,從Visual Studio 2017開發工具「Solution Explorer」視窗選取專案名稱。再從「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入install-package指令:

Install-Package System.Threading.Tasks.Extensions

然後讓你的非同步方法,回傳ValueTask<TResult>型別即可,參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        var value = AddAsync(10, 20);
        Console.WriteLine($"Result : {value.Result}");
    }
    static async ValueTask<int> AddAsync(int x, int y)
    {
        var r = await Task.Run(() => x + y);
        return r;
    }
}

 

數值表達語法

有時我們會需要進行一些位元的運算,像是搭配 [Flags] Attribute來設計多選效果。可參閱本站此篇文章《http://blogs.uuu.com.tw/Articles/post/2017/06/14/使用列舉與旗標設計多選.aspx》來了解更多設計的細節。以此篇文章的範例舉例,以下定義一個列舉型別,描述影片撥放時段,可能是白天(Day)、晚上(Night)、平日(WeekDay)或假日(Holiday),例如以下C# 6 語法程式碼,列舉上方套用[Flags] Attribute,而Main方法中宣告一個myOptions變數,使用「|」(OR)運算子設定兩個值Day與Night:

class Program
{
    [Flags]
    public enum ShowOptions
    {
        Day = 1,
        Night = 2,
        WeekDay = 4,
        Holiday = 8
    }
    static void Main(string[] args)
    {
        ShowOptions myOptions = ShowOptions.Day | ShowOptions.Night;
        Console.WriteLine(myOptions); //Day, Night
        Console.WriteLine((int)myOptions); //3
    }
}

 

若是C# 7可以使用「0b」開始來表達位元資料,它代表二進位(Binary)數字:

class Program
{
    [Flags]
    public enum ShowOptions
    {
        Day = 0b00000001,
        Night = 0b00000010,
        WeekDay = 0b00000100,
        Holiday = 0b00001000
    }

    static void Main(string[] args)
    {
        ShowOptions myOptions = ShowOptions.Day | ShowOptions.Night;
        Console.WriteLine(myOptions); //Day, Night
        Console.WriteLine((int)myOptions); //3
    }
}

 

程式中太多「0」了,看了不免眼花瞭亂我們可以加上數字分隔符號(_)區隔之,參考以下範例程式碼:

[Flags]
public enum ShowOptions
{
    Day = 0b0000_0001,
    Night = 0b0000_0010,
    WeekDay = 0b000_00100,
    Holiday = 0b0000_1000
}

數字分隔符號(_)可以隨意出現多次,參考以下範例程式碼:

[Flags]
public enum ShowOptions
{
     Day = 0b00_000_001,
     Night = 0b00_000_010,
     WeekDay = 0b0_000_100,
     Holiday = 0b00_001_000
}

 

除了int、long型別之外,數字分隔符號(_)還可以使用在decimal、float、double等等型別,參考以下範例程式碼:

double salary1 = 22_000.00;
Console.WriteLine(salary1);
float salary2 = 22_000.00F;
Console.WriteLine(salary2);
decimal salary3 = 22_000.00M;
Console.WriteLine(salary3);

Ref locals and returns(傳參考區域變數與傳參考回傳值)

在C# 中方法提供ref;out類型的參數,允許傳遞參考(Pass by Reference),而新的C# 7「Ref locals and return(傳參考區域變數與傳參考回傳值)」功能現在讓方法可以回傳變數的參考(Return by reference),這個功能可以讓程式儘量避免複製值的動作,取而代之的是,可以透過參考(Reference)直接存取到特定記憶體內容,這樣可以讓程式更有效率,特別適用在數學計算上,例如回傳矩陣(Matrix)某個項目的參考到呼叫端。

參考以下範例程式碼,建立一個Employee物件,設定Age屬性為「50」,接著將Employee物件傳到GetAge方法,這個方法會回傳Age屬性值:

class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee() { Age = 50 };
        int GetAge(Employee emp)
        {
            return emp.Age;
        }
        int age = GetAge(employee);
        Console.WriteLine(age);
        age = 999;
        Console.WriteLine(employee.Age);
    }
}
class Employee
{
    public int Age;
}

 

這個範例一執行,Main方法中兩個Console.WriteLine印出的答案分別是「50」、「50」。在C# 7我們可以撰寫程式碼,回傳欄位(Field)的參考(Reference),參考以下範例程式碼:

class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee() { Age = 50 };
        ref int GetAge(Employee emp)
        {
            return ref emp.Age;
        }
        int x = GetAge(employee);
        Console.WriteLine(x);
        x = 999;
        Console.WriteLine(employee.Age);
    }
}
class Employee
{
    public int Age;
}

 

在GetAge方法使用ref 關鍵字,然後在方法中return 這行也必需要加上ref關鍵字,表示要回傳參考(return by reference),否則編譯將會失敗。

這次執行程式碼,兩個Console.WriteLine印出的答案分別是「50」、「50」。因為「int x」這行變數宣告,會將方法中「return ref」這行回傳的emp.Age內的值,複製到x變數的記憶體(by value),所以當你修改x為「999」時,是修改到x記憶體的內容,不是emp.Age的內容。x此時不算是傳參考區域變數(ref locals)。

參考以下範例程式碼,現在在x區域變數明確地加上「ref」修飾詞;這種區域變數稱做「參考區域變數(ref locals)」,讓方法回傳參考時,x會記住參考:

 

class Program
{
    static void Main(string[] args)
    {
        var employee = new Employee() { Age = 50 };
        ref int GetAge(Employee emp)
        {
            return ref emp.Age;
        }
        ref int x = ref GetAge(employee);
        Console.WriteLine(x);
        x = 999;
        Console.WriteLine(employee.Age);
    }
}
class Employee
{
    public int Age;
}

 

這次執行程式,兩個Console.WriteLine印出的答案分別是「50」、「999」。當你修改x的值為「999」時,這次是修改到emp.Age記憶體的內容。

特別要注意,宣告ref 變數時,需要順帶初始化,例如這行程式碼:

ref int x = ref GetAge(employee);

不可以將宣告和初始化分開寫兩行,以下程式碼將不能夠編譯:

ref int x = 0;
x = ref GetAge(employee);

另外宣告x區域變數這行程式,也可以改用var語法宣告,讓編譯器自動判斷型別:

ref var x = ref GetAge(employee);

目前「傳參考區域變數與傳參考回傳值(Ref locals and returns)」功能只可用在變數、物件欄位(Field),陣列(Array),不可以用在物件屬性(Property)、事件(Event)、List集合…等等。

C# 7.1新功能概覽

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N171118903
出刊日期: 2017/11/29

本篇文章將介紹C# 7.1版新語法,包含非同步Main方法、Default Literal、推論Tuple名稱、泛型模式比對等等主題。

 

C# 專案屬性語言版本設定

預設在Visual Studio 2017建立專案時,C# 專案屬性的語言版本設定是「C# latest major version (default)」,若要使用到C# 7.1 新語法,必需更改這個設定值,請參考下圖所示。

clip_image002

圖 1:設定C# 專案屬性的語言版本。

我們以.NET Core 類型的主控台應用程式(Console)程式來說明如何設定C# 7.1 版語言版本,使用Visual Studio 2017建立一個「Console App(.NET Core)」專案,請參考下圖所示:

clip_image004

圖 2:建立C#測試專案。

使用滑鼠選取「Solution Explorer」視窗,在專案名稱上按滑鼠右鍵,從快捷選單中選取「Properties」選項,開啟屬性視窗。選取左方「Build」分頁,然後點選右方的「Advanced」按鈕,請參考下圖所示:

clip_image006

圖 3:設定C# 專案屬性的語言版本。

在「Advanced Build Settings」對話盒中,將「Language version」設定為「C# 7.1」,如此便可以使用新語法了,請參考下圖所示:

clip_image008

圖 4:設定C# 專案屬性的語言版本為7.1版。

接著我們來看看C# 7.1新增的新語法。

 

非同步Main方法

「Main」方法是C#程式碼的進入點,一個典型的C# Main方法如下程式碼:

 

class Program
{
    static void Main(string[] args)
    {
    }
}

「Main」方法可以回傳數值型別,通常以數值來表示程式執行結果,參考以下範例程式碼:

class Program {
    static int Main(string[] args) {
        return 0;
    }
}

「Main」方法可以沒有參數,例如以下程式碼範例:

class Program
{
    static int Main()
    {
        return 0;
    }
}
 

 

「Main」方法可以沒有參數,回傳int型別,例如以下程式碼範例:

class Program
{
    static int Main()
    {
        return 0;
    }
}

 

在C# 7.1版之前,「Main」方法不可以加註「async」關鍵字,例如在C# 7版之前若「Main」方法加註「async」關鍵字:

static async Task Main( string[] args ) {

}

則編譯程式碼時,會出現以下錯誤訊息:

CS5001 C# Program does not contain a static 'Main' method suitable

C# 7.1 版現在支援了非同步「Main」方法,可以在方法中搭配「await」關鍵字等待非同步的工作,參考以下程式碼範例,在非同步「Main」方法中利用「HttpClient」類別下載網站首頁內容,然後在方法中使用「await」關鍵字等待執行結果:

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace CS71 {
  class Program {
    static async Task Main( string[] args ) {
      HttpClient client = new HttpClient( );
      Task<string> mytask = client.GetStringAsync( "http://www.microsoft.com" );
      string page = await mytask;
      Console.WriteLine( page );
    }
  }
}

 

此範例的執行結果請參考下圖所示:

clip_image010

圖 5:非同步Main方法。

Main方法可以回傳Task<T>,搭配async await關鍵字,例如以下範例程式碼:

class Program {
  static async Task<int> Main( string[] args ) {
    HttpClient client = new HttpClient( );
    Task<string> mytask = client.GetStringAsync( "http://www.microsoft.com" );
    string page = await mytask;
    Console.WriteLine( page.Length );
    return page.Length;
  }
}

Main方法可以回傳Task<T>,例如以下範例程式碼:

class Program {
  static Task<int> Main( string[] args ) {
    HttpClient client = new HttpClient( );
    Task<string> mytask = client.GetStringAsync( "http://www.microsoft.com" );
    string page = mytask.Result;
    Console.WriteLine( page.Length );
    return Task.FromResult( page.Length ) ;
  }
}

Default Literal

通常參考型別初始化時,會設為「null」;實值型別初始化時會設定為特定的值,如數值型別設定為「0」,Default Literal用來取得特定型別的預設值,此型別可以是實值型別(Value Type)或是參考型別(Reference Type)。在前版C# 要取得型別預設值,可以利用「default(T)」語法,例如以下範例程式碼:

string d1 = default( string );
Console.WriteLine( String.IsNullOrEmpty( d1 ) ); // true

Program d2= default( Program );
Console.WriteLine( d2 == null ); // true

bool d3 = default( bool );
Console.WriteLine( d3 ); // false

int d4 = default( int );
Console.WriteLine( d4 ); // 0

 

「default」關鍵字特別適用於使用var關鍵字宣告變數的情境,例如以下範例程式碼:

var d1 = default( string );

Console.WriteLine( String.IsNullOrEmpty( d1 ) ); // true

var d2 = default( Program );

Console.WriteLine( d2 == null ); // true

var d3 = default( bool );

Console.WriteLine( d3 ); // false

var d4 = default( int );

Console.WriteLine( d4 ); // 0

現在C# 7.1 版可以直接使用「default」關鍵字,不需要再「default」關鍵字之後再描述型別,讓程式碼更為簡單,例如以下範例程式碼:

string d1 = default;

Console.WriteLine( String.IsNullOrEmpty( d1 ) ); // true

Program d2= default;

Console.WriteLine( d2 == null ); // true

bool d3 = default;

Console.WriteLine( d3 ); // false

int d4 = default;

Console.WriteLine( d4 ); // 0

但不能夠搭配var關鍵字使用,以下程式碼將無法編譯:

var d1 = default; // Error

 

推論Tuple名稱

C# 7為Tuple加入許多新功能,可以參考本站《C# 7新功能概覽 - 1》一文的說明。這個新功能就根本節的標題一樣自動推論Tuple項目的名稱,參考以下範例程式碼建立一個newRect匿名型別,它的屬性值來自於「Rectangle」型別的「r」物件,預設newRect匿名型別就會自動產生兩個和Rectangle屬性同名的屬性「Area」與「Perimeter」,然後我們就可以利用「newRect.Area」與「newRect.Perimeter」語法來存取他們的值:

class Program {
  static void Main( string[] args ) {
    int l = 40;
    int w = 20;
    Rectangle r = new Rectangle( l , w );
    Console.WriteLine( $"Area = {r.Area}" );
    Console.WriteLine( $"Perimeter = {r.Perimeter}" );

    var newRect = new { r.Area , r.Perimeter};
    Console.WriteLine( $"Area = {newRect.Area}" );
    Console.WriteLine( $"Perimeter = {newRect.Perimeter}" );
  }
}
public class Rectangle {
  public Rectangle( int len , int width ) {
    this.Area = len * width;
    this.Perimeter = ( len + width ) * 2;
  }
  public int Area { get; set; }
  public int Perimeter { get; set; }
}


此範例的執行結果請參考下圖所示:

clip_image012

圖 6:推論Tuple名稱。

C# 7.1 版可以改寫如下,建立一個Tuple(newRect),其項目的值來自於Rectangle物件,其項目的名稱自動推論:

class Program {
  static void Main( string[] args ) {
    int l = 40;
    int w = 20;
    Rectangle r = new Rectangle( l , w );
    Console.WriteLine( $"Area = {r.Area}" );
    Console.WriteLine( $"Perimeter = {r.Perimeter}" );

    var newRect = ( r.Area , r.Perimeter);
    Console.WriteLine( $"Area = {newRect.Area}" );
    Console.WriteLine( $"Perimeter = {newRect.Perimeter}" );
  }
}
public class Rectangle {
  public Rectangle( int len , int width ) {
    this.Area = len * width;
    this.Perimeter = ( len + width ) * 2;
  }
  public int Area { get; set; }
  public int Perimeter { get; set; }
}

 

使用Tuple的好處是,它是實值型別,比起匿名型別執行效能要來的好。

 

泛型模式比對

C# 7 新增模式比對(Pattern Matching)語法,但不支援泛型的比對,C# 7.1 新的泛型模式比對(Pattern Matching With Generics)則改良原有語法,舉例來說,參考以下範例程式碼:

class Program {
   static void Main( string[] args ) {

     List<Employee> employees = new List<Employee>( ) {
       new Sales { Id = 1 , Name = "Mary" , Bonus = 1000 } ,
       new Sales { Id = 2 , Name = "Candy" , Bonus = 2000 } ,
       new CEO { Id = 3 , Name = "Candy" , Allowance = 10000 }
     };

     foreach ( var emp in employees ) {
       ShowInfo( emp );
     }
   }
   static void ShowInfo<T>( T t ) where T : Employee {
     if ( t is Sales s ) {
       Console.WriteLine( $"Sales : ( {t.Id} , {t.Name} ) , Bonus : {s.Bonus}" );
     }
     else if ( t is CEO c ) {
       Console.WriteLine( $"CEO : ( {t.Id} , {t.Name} ) , Allowance : {c.Allowance}" );
     }
   }
}
public abstract class Employee {
   public int Id { get; set; }
   public string Name { get; set; }
}

public class Sales : Employee {
   public int Bonus { get; set; }
}

public class CEO : Employee {
   public int Allowance { get; set; }
}

 

此範例的執行結果請參考下圖所示:

clip_image014

圖 7:泛型模式比對。

上述的ShowInfo<T>() 方法有一個泛型型別參數,ShowInfo<T>() 方法中使用到generic pattern matching語法,使用is樣式運算式(Pattern Expression)來比對型別,並隱含轉換成Sales或CEO型別。這段程式碼在C# 7 編譯時,會得到以下錯誤訊息:

An expression of type 'T' cannot be handled by a pattern of type 'Sales' in C# 7. Please use language version 7.1 or greater.

從資料庫動態載入樹狀結構選單

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N171219002
出刊日期: 2017/12/27

在這篇文章中,將要介紹如何在ASP.NET Core MVC專案中,從資料庫動態載入選單項目,並利用一個開放源碼、且支援jQuery的Gijgo tree控制項,套用Bootstrap的樣式以樹狀結構的方式來顯示網站選單。

預設Visual Studio 2017的「ASP.NET Core Web Application」範本專案中,有包含Bootstrap套件與Entity Framework Core套件,可以省略一些套件安裝與設計的步驟,本文將從建立Menu模型開始,然後利用Entity Framework Core Code First建立資料庫結構,最後透過Entity Framework Core查詢出資料表中的選單資料,套用Bootstrap範本,利用Gijgo tree控制項來顯示網站的選單。

Gijgo是一組開放源碼的JavaScript控制項,以jQuery為基礎,可以搭配Bootstrap、Material Design與Font Awesome套件,官網在「http://gijgo.com/」。Gijgo包含多種常見於網站的使用者介面,例如Grid、Tree、Dialog等等。

建立ASP.NET Core MVC專案

1. 啟動Visual Studio 2017開發環境,從Visual Studio開發工具「File」-「New」-「Project」項目,在「New Project」對話盒中,確認視窗上方.NET Framework的目標版本為「.NET Framework 4.6.1」或以上版本,選取左方「Installed」清單-「Templates」-「Visual C#」程式語言,從「.NET Core」分類中,選取「ASP.NET Core Web Application」。設定專案名稱為「TreeDemo」,設定專案存放路徑,然後按下「OK」鍵,請參考下圖所示:

clip_image002

圖 1:建立ASP.NET Core MVC專案。

2. 在「New ASP.NET Core Web Application」對話盒中,確認左上方的清單選取「.NET Core」,右上方的清單ASP.NET Core版本為「ASP.NET Core 2.0」,選取下方的「Web Application ( Model – View – Controller) 」樣版專案,清除勾選下方的「Enable Docker Support」核取方塊,確定右方的「Authentication」項目設定為「No Authentication」,然後按下「OK」按鈕建立專案,請參考下圖所示:

clip_image004

圖 2:建立Web Application ( Model – View – Controller) 」樣版專案。

3. 從「Solution Explorer」視窗 -「Models」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」項目,從「Add New Item」對話盒中,選取「Code」分類下的,Class項目,然後在下方將Name設定為「Menu」最後按下「Add」按鈕,請參考下圖所示:

clip_image006

圖 3:加入Menu模型。

4. 在「Menu」類別檔案之中,為「Menu」類別定義「MenuId」、「Title」、「Url」與「ParentMenuId」屬性:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TreeDemo.Models
{
    public class Menu
    {
        public int MenuId { get; set; }
        public string Title { get; set; }
        public string Url { get; set; }
        public int ParentMenuId { get; set; }

    }
}

 

5. 從Visual Studio 2017開發工具 -「Solution Explorer」視窗 - 「TreeDemo專案」-「Models」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,開啟「Add New Item」對話盒,請參考下圖所示:

clip_image008

圖 4:建立控制器。

6. 在「Add New Item」對話盒中選取「Class」項目,設定類別名稱為「MyDbContext」,然後按下「Add 」按鈕,請參考下圖所示:

clip_image010

圖 5 : 加入類別。

7. 在「MyDbContext」類別定義一個「Menu」屬性以透過Entity Framework Core存取資料庫中「Menu」資料表資料:

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TreeDemo.Models
{
    public class MyDbContext : DbContext
    {
        public MyDbContext(DbContextOptions<MyDbContext> options)
            : base(options)
        {
        }
        public DbSet<TreeDemo.Models.Menu> Menu { get; set; }
    }
}

 

 

8. 在專案中根目錄下「appsettings.json」檔案,加入設定,儲存資料庫連接字串:

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "ConnectionStrings": {
    "MyDbContext": "Server=(localdb)\\mssqllocaldb;Database=MyDbContext;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

 

9. 在專案中根目錄下「Startup.cs」檔案上方引用以下命名空間:

using Microsoft.EntityFrameworkCore;

using TreeDemo.Models;

10. 在專案中根目錄下「Startup.cs」檔案中的「ConfigureServices」方法內加入以下程式碼,以新增服務:

public void ConfigureServices(IServiceCollection services)
      {
          services.AddMvc();

          services.AddDbContext<MyDbContext>(options =>
                  options.UseSqlServer(Configuration.GetConnectionString("MyDbContext")));
      }

11. 選取Visual Studio開發工具「Build」-「Build Solution」編譯目前的專案,確認程式碼能正確編譯。

12. 開啟「Package Manager Console」視窗,在「Solution Explorer」視窗選取專案名稱。從Visual Studio 2017開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入add-migration指令:

add-migration initial

執行結果參考下圖所示:

clip_image012

圖 6:使用Code First。

接著Visual Studio會在專案中建立一個Migrations資料夾,包含一些C#檔案。其中有一個XXX_initial.cs檔案內容大約如下:

using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;

namespace TreeDemo.Migrations
{
    public partial class initial : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Menu",
                columns: table => new
                {
                    MenuId = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    ParentMenuId = table.Column<int>(type: "int", nullable: false),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Url = table.Column<string>(type: "nvarchar(max)", nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Menu", x => x.MenuId);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Menu");
        }
    }
}

 

還有一個MyDbContextModelSnapshot檔案,程式參考如下:

// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
using TreeDemo.Models;

namespace TreeDemo.Migrations
{
    [DbContext(typeof(MyDbContext))]
    partial class MyDbContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
#pragma warning disable 612, 618
            modelBuilder
                .HasAnnotation("ProductVersion", "2.0.0-rtm-26452")
                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

            modelBuilder.Entity("TreeDemo.Models.Menu", b =>
                {
                    b.Property<int>("MenuId")
                        .ValueGeneratedOnAdd();

                    b.Property<int>("ParentMenuId");

                    b.Property<string>("Title");

                    b.Property<string>("Url");

                    b.HasKey("MenuId");

                    b.ToTable("Menu");
                });
#pragma warning restore 612, 618
        }
    }
}

13. 選取Visual Studio開發工具「Build」-「Build Solution」編譯目前的專案,確認程式碼能正確編譯。

14. 開啟「Package Manager Console」視窗,然後在提示字元中輸入指令,以更新資料庫:

update-database

得到的執行結果如下圖所示:

clip_image014

圖 7:更新資料庫。

預設資料庫會建立在「C:\Users\登入帳號」資料夾中。

15. 開啟「Server Explorer」視窗,在「Data Connections」項目上按滑鼠右鍵,從快捷選單之中選取「Add Connection」項目,請參考下圖所示:

clip_image016

圖 8:連結到資料庫。

16. 若出現「Choose Data Source」視窗,則選擇「Microsoft SQL Server」,然後按「Continue」按鈕,請參考下圖所示:

clip_image018

圖 9:選取SQL Server資料庫。

17. 在「Add Connection」視窗中,設以下屬性,然後按下「OK」按鈕,請參考下圖所示:

l 資料來源 (Data Source) :Microsoft SQL Server (SqlClient)。

l Server name欄位:輸入「(localdb)\mssqllocaldb」。

l Authentication:選取「Windows Authentication」。

l Select or enter a database name欄位:選擇「MyDbContext」資料庫。

clip_image020

圖 10:設定連接資訊。

18. 檢視新建立的資料表與欄位資訊,參考如下:

clip_image022

圖 11:檢視新建立的資料表與欄位資訊。

19. 檢查資料庫結構描述資訊。展開「Server Explorer」視窗-「Data Connections」-「MyDbContext-XXX」資料庫 -「Tables」- 「Menu」資料表,在「Menu」資料表上按滑鼠右鍵,從快捷選單中選取「Show Table Data」顯示資料表所有資料,請參考下圖所示:

clip_image024

圖 12:查詢資料表資料。

20. 參考下圖,新增幾筆資料到資料庫「Menu」資料表:

clip_image026

圖 13:「Menu」資料表中的資料。

安裝Gilgo套件

21. 從「Solution Explorer」- 專案資料夾上方按滑鼠右鍵,從快捷選單選擇「Manage Bower Packages」選項,開啟「Manage Bower Packages」對話盒:

clip_image028

圖 14:開啟「Manage Bower Packages」對話盒。

22. 在「Manage Bower Packages」對話盒點選「Browse」項目,在上方的文字方塊中,輸入「gijgo」關鍵字做搜尋,安裝「gijgo 」1.6.1版本:

clip_image030

圖 15:安裝「gijgo 」1.6.1版本。

Bower會將相依的套件安裝到專案「wwwroot\lib\gijgo」資料夾之中,並在「bower.json」檔案中加入以下設定:

{
  "name": "asp.net",
  "private": true,
  "dependencies": {
    "bootstrap": "3.3.7",
    "jquery": "2.2.0",
    "jquery-validation": "1.14.0",
    "jquery-validation-unobtrusive": "3.2.6",
    "gijgo": "v1.6.1"
  },
  "resolutions": {
    "jquery": ">=1.8"
  }
}

23. 從「Solution Explorer」視窗 -「Models」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」項目,從「Add New Item」對話盒中,選取「Code」分類下的,Class項目,然後在下方將Name設定為「TreeNode」最後按下「Add」按鈕

clip_image032

圖 16:加入TreeNode模型。

24. 在「TreeNode」類別檔案之中,為「TreeNode」類別定義「id」、「text」、「imageUrl」與「children」屬性:

public class TreeNode
    {
        public int id { get; set; }
        public string text { get; set; }
        public string imageUrl { get; set; }
        public List<TreeNode> children { get; set; }
    }

25. 在「HomeController」類別宣告一個「MyDbContext」 型別的private變數,將名稱設定為「_context」:

private readonly MyDbContext _context;

26. 在「HomeController」類別加入一個建構函式:

public HomeController(MyDbContext context)
{
    _context = context;
}

27. 在「HomeController」類別加入「GetMenus」與「GetChildNode」方法:

public JsonResult GetMenus()
{
    List<Menu> menuList = _context.Menu.OrderBy(x => x.ParentMenuId).ThenBy(x => x.MenuId).ToList();
    List<TreeNode> siteMenu = menuList.Where(m => m.ParentMenuId == 0).OrderBy(m => m.MenuId)
        .Select(node => new TreeNode
        {
            id = node.MenuId,
            text = node.Title,
            imageUrl= "/images/FolderOptions.ico",
            children = GetChildNode(menuList, node.MenuId)
        }).ToList();

    return this.Json(siteMenu);
}
private List<TreeNode> GetChildNode(List<Menu> menuList, int parentId)
{
    return menuList.Where(l => l.ParentMenuId == parentId).OrderBy(l => l.MenuId)
        .Select(node => new TreeNode
        {
            id = node.MenuId,
            text = node.Title,
            imageUrl = "/images/arrowright.ico",
            children = GetChildNode(menuList, node.MenuId)
        }).ToList();
}

28. 選取Visual Studio開發工具「Build」-「Build Solution」編譯目前的專案,確認程式碼能正確編譯。

29. 按CTRL+F5執行網站首頁(請注意:以下埠號「59492」可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:

http://localhost:59492/Home/GetMenus

此時會得到以下JSON資料:

[{"id":1,"text":"MyWebSite","imageUrl":"/images/FolderOptions.ico","children":[{"id":2,"text":"Home","imageUrl":"/images/arrowright.ico","children":[]},{"id":3,"text":"About","imageUrl":"/images/arrowright.ico","children":[]},{"id":4,"text":"Contact","imageUrl":"/images/arrowright.ico","children":[]}]},{"id":5,"text":"Menus","imageUrl":"/images/FolderOptions.ico","children":[{"id":6,"text":"List","imageUrl":"/images/arrowright.ico","children":[]},{"id":7,"text":"Create","imageUrl":"/images/arrowright.ico","children":[]}]}]

執行結果請參考下圖所示:

clip_image034

圖 17:取得Json資料。

 

30. 修改「\Views\Home\Index」檢視,加入以下程式碼:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title>Index</title>
    <script src = "~/lib/jquery/dist/jquery.js"></script>
    <script src = "~/lib/gijgo/dist/modular/js/core.js"></script>
    <link href = "~/lib/gijgo/dist/modular/css/core.css" rel = "stylesheet" />
    <link href = "https://fonts.googleapis.com/icon?family=Material+Icons" rel = "stylesheet" type = "text/css">
    <script src = "~/lib/gijgo/dist/modular/js/tree.js"></script>
    <link href = "~/lib/gijgo/dist/modular/css/tree.css" rel = "stylesheet" />
</head>
<body>
    <div class = "container-fluid">
        <div class = "row">
            <div id = "tree"></div>
        </div>
    </div>
    <script>
        $('#tree').tree({
            dataSource: '/Home/GetMenus'
        });
    </script>
</body>
</html>

31. 按CTRL+F5執行網站首頁(請注意:以下埠號「59492」可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:

http://localhost:59492/Home/Index

執行結果請參考下圖所示:

clip_image036

圖 18:Tree選單。

ASP.NET Identity Core入門 - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N180119101
出刊日期: 2018/1/24

在這篇文章中,將延續《ASP.NET Identity Core入門- 1》一文的情境,介紹如何在ASP.NET Core網站專案中,使用ASP.NET Core Identity設計會員系統,以完成會員註冊與會員登入的功能。

 

設計ViewModel模型

下一個步驟是在專案中新增「AccountViewModels」資料夾集中管理安控相關的ViewModel。從Visual Studio 2017開發工具 -「Solution Explorer」視窗 - 專案 - 「Models」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「AccountViewModels」。

新增「RegisterViewModel」類別,從「Solution Explorer」視窗 -「Models」-「AccountViewModels」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」項目,從「Add New Item」對話盒中,選取「Code」分類下的「Class」項目,然後在下方將「Name」設定為「RegisterViewModel」,最後按下「Add」按鈕,請參考下圖所示:

clip_image002

圖 1:新增「RegisterViewModel」類別。

「RegisterViewModel」類別定義註冊網頁要顯示「Email」、「Password」、「ConfirmPassword」與「NickName」項目讓使用者輸入,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace IdentityDemo.Models.AccountViewModels {
  public class RegisterViewModel {
    [Required]
    [EmailAddress]
    [Display( Name = "Email" )]
    public string Email { get; set; }

    [Required]
    [StringLength( 100 , ErrorMessage = "The {0} must be at least {2} and at max {1} characters long." , MinimumLength = 6 )]
    [DataType( DataType.Password )]
    [Display( Name = "Password" )]
    public string Password { get; set; }

    [DataType( DataType.Password )]
    [Display( Name = "Confirm password" )]
    [Compare( "Password" , ErrorMessage = "The password and confirmation password do not match." )]
    public string ConfirmPassword { get; set; }

    [Required]
    [StringLength( 100 , ErrorMessage = "The {0} must be at least {2} and at max {1} characters long." , MinimumLength = 6 )]
    [Display( Name = "Nick Name" )]
    public string NickName { get; set; }
  }
}

 

設計控制器

接下來的步驟要在專案中新增兩個控制器:「HomeController」與「AccountController」。

從Visual Studio 2017開發工具 -「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「Controllers」。

從「Solution Explorer」視窗 - 「Controllers」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Scaffolded Item」或「Controllers」項目。在「Add Scaffold」對話盒中選取「MVC Controller - Empty」項目,然後按下「Add 」按鈕,請參考下圖所示:

clip_image004

圖 2:加入控制器。

在「Add Controller」對話盒中,設定控制器名稱為「HomeController」;然後按下「Add 」按鈕,請參考下圖所示:

clip_image006

圖 3:設定控制器名稱。

預設「HomeController」類別中會自動加入以下程式碼,其中包含一個「Index」方法,修改程式碼在類別上方套用「Authorize」Attribute:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace IdentityDemo.Controllers {
  [Authorize]
  public class HomeController : Controller {
    public IActionResult Index( ) {
      return View( );
    }
  }
}


 

建立「Index」檢視。將游標停留在控制器程式設計畫面「Index」方法之中,按滑鼠右鍵,從快捷選單選取「Add View」,請參考下圖所示。

clip_image008

圖 4:建立「Index」檢視。

在「Add View」對話盒中,設定:

  • View name:「Index」。
  • Template:「Empty (without model)」。
  • Model class:不設定。
  • 清除勾選所有核取方塊。

然後按下「Add」按鈕。Visual Studio 2017便會在「Views\Home」資料夾下,新增一個「Index.cshtml」檔案,請參考下圖所示:

clip_image010

圖 5:建立「Index」檢視。

改Index檢視的內容。從「Solution Explorer」視窗專案名稱下,雙擊「Views\Home\Index.cshtml」檔案,開啟設計畫面,在<body>下<div>標籤中,新增一個<h1>標籤,顯示「Home」字串:

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Index </title>
</head>
<body>
    <h1> Home </h1>

</body>
</html>


 

在「Views」資料夾內加入一個「_ViewImports.cshtml」檔案。從「Solution Explorer」視窗專案名稱下「Views」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」選項,選取「Web」分類中的「MVC View Imports Page」,檔案名稱設定為「_ViewImports.cshtml」,然後按下「Add」按鈕新增檔案,請參考下圖所示:

clip_image012

圖 6:建立「_ViewImports.cshtml」檔案。

修改新建立的「_ViewImports.cshtml」檔案,在其中加入以下程式碼,引用命名空間,並註冊Tag Helper:

@using Microsoft.AspNetCore.Identity

@using IdentityDemo.Models

@using IdentityDemo.Models.AccountViewModels

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

 

設計註冊功能

AccountController負責管理使用者註冊以及登入登出功能,首先先完成註冊功能。從「Solution Explorer」視窗 - 「Controllers」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Scaffolded Item」或「Controllers」項目。在「Add Scaffold」對話盒中選取「MVC Controller - Empty」項目,然後按下「Add 」按鈕。在「Add Controller」對話盒中,設定控制器名稱為「AccountController」;然後按下「Add 」按鈕建立類別,接著加入以下程式碼,利用相依性插入(Dependency Injection)加入「UserManager<ApplicationUser>」與「SignInManager<ApplicationUser>」:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using IdentityDemo.Models;
using IdentityDemo.Models.AccountViewModels;

namespace IdentityDemo.Controllers {

  public class AccountController : Controller {
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    public AccountController(
            UserManager<ApplicationUser> userManager ,
            SignInManager<ApplicationUser> signInManager ) {
      _userManager = userManager;
      _signInManager = signInManager;

    }

    [HttpGet]
    public IActionResult Register( ) {
      return View( );
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Register( RegisterViewModel model ) {

      if ( ModelState.IsValid ) {
        var user = new ApplicationUser {
          UserName = model.Email ,
          Email = model.Email ,
          NickName = model.NickName
        };
        var result = await _userManager.CreateAsync( user , model.Password );
        if ( result.Succeeded ) {
          await _signInManager.SignInAsync( user , isPersistent: false );
          return RedirectToAction( "Index" , "Home" );
        }

        foreach ( var error in result.Errors ) {
          ModelState.AddModelError( string.Empty , error.Description );
        }
      }
      return View( model );
    }
  }
}

「UserManager」管理使用者資訊;而「SignInManager」則負責登入與登出。「AccountController」類別的「Register」方法負責使用者註冊的邏輯。

建立「Register」檢視。將游標停留在「AccountController」控制器程式設計畫面「Register」方法之中,按滑鼠右鍵,從快捷選單選取「Add View」,請參考下圖所示:

clip_image014

圖 7:建立「Register」檢視。

在「Add View」對話盒中,設定:

  • View name:「Register」。
  • Template:「Create」。
  • Model class:設定為「RegisterViewModel」。
  • Data context class:不設定。
  • 清除勾選所有核取方塊。

然後按下「Add」按鈕。Visual Studio 2017便會在「Views\Account」資料夾下,新增一個「Register.cshtml」檔案,請參考下圖所示:

clip_image016

圖 8:建立「Register」檢視。

預設將產生以下Register檢視程式碼:

@model IdentityDemo.Models.AccountViewModels.RegisterViewModel

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width = device-width" />
    <title> Register </title>
</head>
<body>

<h4> RegisterViewModel </h4>
<hr />
<div class = "row">
    <div class = "col-md-4">
        <form asp-action = "Register">
            <div asp-validation-summary = "ModelOnly" class = "text-danger"> </div>
            <div class = "form-group">
                <label asp-for = "Email" class = "control-label"> </label>
                <input asp-for = "Email" class = "form-control" />
                <span asp-validation-for = "Email" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "Password" class = "control-label"> </label>
                <input asp-for = "Password" class = "form-control" />
                <span asp-validation-for = "Password" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "ConfirmPassword" class = "control-label"> </label>
                <input asp-for = "ConfirmPassword" class = "form-control" />
                <span asp-validation-for = "ConfirmPassword" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "NickName" class = "control-label"> </label>
                <input asp-for = "NickName" class = "form-control" />
                <span asp-validation-for = "NickName" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <input type = "submit" value = "Create" class = "btn btn-default" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action = "Index">Back to List</a>
</div>

</body>
</html>

 

修改產生出來的Register檢視的內容。從「Solution Explorer」視窗專案名稱下,雙擊「Views\Account\Register.cshtml」檔案,開啟設計畫面,加入以下程式碼移除不必要的「Back to List」連結,並修改標題:

@model IdentityDemo.Models.AccountViewModels.RegisterViewModel
@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Register </title>
</head>
<body>
    <h4> Register </h4>
    <hr />
    <div class = "row">
        <div class = "col-md-4">
            <form asp-action = "Register">
                <div asp-validation-summary = "ModelOnly" class = "text-danger"> </div>
                <div class = "form-group">
                    <label asp-for = "Email" class = "control-label"> </label>
                    <input asp-for = "Email" class = "form-control" />
                    <span asp-validation-for = "Email" class = "text-danger"> </span>
                </div>
                <div class = "form-group">
                    <label asp-for = "Password" class = "control-label"> </label>
                    <input asp-for = "Password" class = "form-control" />
                    <span asp-validation-for = "Password" class = "text-danger"> </span>
                </div>
                <div class = "form-group">
                    <label asp-for = "ConfirmPassword" class = "control-label"> </label>
                    <input asp-for = "ConfirmPassword" class = "form-control" />
                    <span asp-validation-for = "ConfirmPassword" class = "text-danger"> </span>
                </div>
                <div class = "form-group">
                    <label asp-for = "NickName" class = "control-label"> </label>
                    <input asp-for = "NickName" class = "form-control" />
                    <span asp-validation-for = "NickName" class = "text-danger"> </span>
                </div>
                <div class = "form-group">
                    <input type = "submit" value = "Register" class = "btn btn-default" />
                </div>
            </form>
        </div>
    </div>
</body>
</html>

 

選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。接著按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:

http://localhost:1880/account/register

執行結果參考如下:

clip_image018

圖 9:執行註冊。

試著註冊一個新使用者,然後按下「Register」按鈕,請參考下圖所示:

clip_image020

圖 10:輸入資料。

註冊成功將導向首頁畫面,請參考下圖所示:

clip_image022

圖 11:註冊成功將導向首頁。

檢查資料庫結構描述資訊。展開「Server Explorer」視窗-「Data Connections」-「IdentityDemo資料庫」-「Tables」- 「AspNetUsers」資料表,在「AspNetUsers」資料表上按滑鼠右鍵,從快捷選單中選取「Show Table Data」顯示資料表所有資料,請參考下圖所示:

clip_image024

圖 12:檢查資料庫結構描述資訊。

設計登入功能

接下來說明登入功能的設計。從「Solution Explorer」視窗 -「Models」-「AccountViewModels」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」項目,從「Add New Item」對話盒中,選取「Code」分類下的「Class」項目,然後在下方將「Name」設定為「LoginViewModel」最後按下「Add」按鈕,請參考下圖所示:

clip_image026

圖 13:加入「LoginViewModel」類別。


在「LoginViewModel」類別中加入以下程式碼:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace IdentityDemo.Models.AccountViewModels {
  public class LoginViewModel {
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType( DataType.Password )]
    public string Password { get; set; }
  }
}


修改「AccountController」類別,加入「Login」方法程式碼,叫用SignInManager<ApplicationUser>的「PasswordSignInAsync」方法驗證使用者帳號與密碼是否與資料庫中的相符,若輸入正確的帳號與密碼將被導首頁:

[HttpGet]
public IActionResult Login( ) {
  return View( );
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login( LoginViewModel model ) {
  var result = await _signInManager.PasswordSignInAsync( model.Email , model.Password , false , false );
  if ( result.Succeeded ) {
    return RedirectToAction( "Index" , "Home" );
  } else {
    ModelState.AddModelError( string.Empty , "Invalid login attempt." );
    return View( model );
  }
}


 

建立「Login」檢視。將游標停留在控制器程式設計畫面「Login」方法之中,按滑鼠右鍵,從快捷選單選取「Add View」,請參考下圖所示:

clip_image028

圖 14:建立「Login」檢視。

在「Add View」對話盒中,設定:

  • View name:「Login」。
  • Template:「Create)」。
  • Model class:設定「LoginViewModel」。
  • Data context class:不設定。
  • 清除勾選所有核取方塊。

然後按下「Add」按鈕。Visual Studio 2017便會在「Views\Account」資料夾下,新增一個「Login.cshtml」檔案,請參考下圖所示:

clip_image030

圖 15:建立「Login」檢視。

修改「Login」檢視的內容。從「Solution Explorer」視窗專案名稱下,雙擊「Views\Account\Login.cshtml」檔案,開啟設計畫面,加入以下程式碼:

 

@model IdentityDemo.Models.AccountViewModels.LoginViewModel
@inject SignInManager<ApplicationUser> SignInManager

@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Login </title>
</head>
<body>
    <h4> Login </h4>
    <hr />
    <div class = "row">
        <div class = "col-md-4">
            <form asp-action = "Login">
                <div asp-validation-summary = "ModelOnly" class = "text-danger"> </div>
                <div class = "form-group">
                    <label asp-for = "Email" class = "control-label"> </label>
                    <input asp-for = "Email" class = "form-control" />
                    <span asp-validation-for = "Email" class = "text-danger"> </span>
                </div>
                <div class = "form-group">
                    <label asp-for = "Password" class = "control-label"> </label>
                    <input asp-for = "Password" class = "form-control" />
                    <span asp-validation-for = "Password" class = "text-danger"> </span>
                </div>
                <div class = "form-group">
                    <input type = "submit" value = "Login" class = "btn btn-default" />
                </div>
            </form>
        </div>
    </div>
</body>
</html>

 

最後我們希望使用者登入之後,首頁會顯示歡迎使用者的訊息。修改「HomeController」類別程式碼,類別上方套用「Authorize」Attribute,表示需登入才可以存取首頁:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Http;
using IdentityDemo.Models;

namespace IdentityDemo.Controllers {
  [Authorize]
  public class HomeController : Controller {
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IHttpContextAccessor _context;
    public HomeController( UserManager<ApplicationUser> userManager , IHttpContextAccessor context ) {
      _userManager = userManager;
      _context = context;
    }
    public async Task<IActionResult> Index( ) {
      var user = await _userManager.GetUserAsync( _context.HttpContext.User );
      ViewBag.NickName = user.NickName;

      return View( );
    }
  }
}


修改首頁「Index」檢視的內容。從「Solution Explorer」視窗專案名稱下,雙擊「Views\Home\ Index.cshtml」檔案,開啟設計畫面,加入以下程式碼:

 

@{
  Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Index </title>
</head>
<body>
    <h1> Home </h1>
    <p>Hello , @ViewBag.NickName  [@User.Identity.Name]</p>
</body>
</html>

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),執行結果參考如下,會自動導向登入畫面:

clip_image032

圖 16:登入測試。

登入網站之後,首頁將顯示使用者NickName與電子郵件資訊,請參考下圖所示:

clip_image034

圖 17:登入網站顯示使用者NickName與電子郵件。

設計登出功能

修改「AccountController」類別,在「Login」方法下方,加入以下「Logout」方法程式碼,利用SignInManager的「SignOutAsync」方法登出,登出後將被導向登入畫面:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout( ) {
  await _signInManager.SignOutAsync( );
  return RedirectToAction( nameof( AccountController.Login ) , "Account" );
}

 

除了使用「User.Identity.Name」程式來取得登入帳號資訊之外,也可以利用「UserManager」的「GetUserName」方法,修改「Views\Home\Index」檢視程式如下:

@inject UserManager<ApplicationUser> UserManager

@{
  Layout  =  null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Index </title>
</head>
<body>
    <h1> Home </h1>
    <p>Hello , @ViewBag.NickName  [@UserManager.GetUserName( User )]</p>

    <form asp-area = "" asp-controller = "Account" asp-action = "Logout" method = "post" id = "logoutForm">
        <button type = "submit"> Log out </button>
    </form>
</body>
</html>

 

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。按CTRL+F5執行網站首頁,然後登入網站,執行結果參考如下:

clip_image036

圖 18:登入顯示歡迎資訊。

按下「Log out」按鈕登出後,將回到Login畫面,請參考下圖所示:

clip_image038

圖 19:登出將回到登入畫面。

LINQ語法簡介 - 1

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N180219202
出刊日期: 2018/2/21

撰寫這篇文章的契機是因為最近遇到有些客戶想要學C# 語法,但客戶回饋的訊息是:「因為沒有在用LINQ,所以不想學」。這讓我好生訝異,「LINQ」語法簡單好用,我想客戶是因為對於LINQ不了解才不想用它,否則還有哪個特別的理由會捨棄使用這個語法呢?

LINQ是Language Integrated Query(語言整合查詢)的簡稱,用於存取記憶體中的物件。因為記憶體中物件的資料經常來自於資料庫,例如透過微軟Entity Framework查詢資料,會將查詢出來的資料庫資料轉換成物件,放在集合中,因此感覺上就好像是可以直接查詢資料庫的資料一般。此外LINQ也可以支援XML文件的查詢。

目前微軟的C#與Visual Basic程式語言都支援LINQ語法,如此就可透過一個統一的查詢介面,來查詢不同資料來源的資料,如物件、集合、資料庫、XML甚至服務導向程式。這篇文章將介紹一些常用的語法。

 

LINQ命名空間與類別

「System.Linq」命名空間中包含許多類別與介面支援LINQ查詢,較重要的類別包含「Enumberable」與「Queryable」類別。「Enumberable」類別提供多個靜態方法,用以查詢有實作IEnumerable<T>介面的物件,例如List<T>、Dictionary<T> …等等集合。而「Queryable」類別則用於查詢有實作IQueryable<T>介面的物件,適用於欲查詢的資料型別為未知的情況。例如微軟的Entity Framework便實作了了此介面,提供SQL資料庫資料查詢的功能。

LINQ語法分為兩類:

· 查詢運算式(Query Expression)。

· Lamda擴充方法(Extension Method)。

 

第一個LINQ查詢運算式

LINQ查詢可以在你不熟悉SQL、XQL(XML Query Language)查詢語言的情況下,就可以查詢資料庫或XML文件的內容,我們來看第一個LINQ查詢運算式範例:

using System;
using System.Linq;

namespace LINQDemo {
  class Program {
    static void Main( string [] args ) {
      //指定資料來源
      string [] courses = { "C#" , "Visual Basic" , "LINQ" , "JavaScript" , "ASP.NET" };

      //定義查詢運算式
      var allCourses = from item in courses
                       select item;

      //執行查詢
      foreach ( var item in allCourses ) {
        Console.WriteLine( item );
      }

      Console.ReadLine( );
    }
  }
}


 

在這個範例中,「allCourses」是用來放執行結果的變數。「from」後的「item」代表範圍(Range)變數,「in」後通常接資料序列(Sequence),如IEnumerable或IQueryable類型的集合。「select」則稱為標準查詢運算子(Standard Query Operator)

當這個範例執行,會印出以下結果:

C#
Visual Basic
LINQ
JavaScript
ASP.NET

這個範例使用的語法稱為LINQ 查詢運算式(Query Expression),使用類似SQL的語法來查詢資料,不同的是LINQ查詢語法都是以「from」開始,有別於SQL以「select」開始。要使用LINQ在程式中需要引用「System.Linq」命名空間,預設當你建立C#直專案時便會自動引用。

我們也可以很容易利用「where」查詢運算子來篩選資料,例如以下範例程式碼,將「C」開頭的課程資訊篩選出來:

using System;
using System.Linq;

namespace LINQDemo {
  class Program {
    static void Main( string [] args ) {
      string [] courses = { "C#" , "Visual Basic" , "LINQ" , "JavaScript" , "ASP.NET" };

      var allCourses = from item in courses
                       where item.StartsWith("C")
                       select item;
      foreach ( var item in allCourses ) {
        Console.WriteLine( item );
      }
      Console.ReadLine( );
    }
  }
}


 

這個範例的執行結果如下,印出一個「C#」字串:
C#

 

使用Lamda擴充方法查詢

第二種查詢的方式是透過包含在「Enumerable」或「Queryable」靜態類別中的擴充方法,此種擴充方法又稱Lamda語法,或Fluent語法,例如上例程式可以改寫如下,得到相同的執行結果:

using System;
using System.Linq;

namespace LINQDemo {
  class Program {
    static void Main( string [] args ) {
      string [] courses = { "C#" , "Visual Basic" , "LINQ" , "JavaScript" , "ASP.NET" };

      var allCourses = courses.Where( c => c.Contains( "C" ) );

      foreach ( var item in allCourses ) {
        Console.WriteLine( item );
      }
      Console.ReadLine( );
    }
  }
}

此範例的「Where」是擴充方法,而「c => c.Contains( "C" )」則是Lamda語法。

使用「LINQPad」工具

除了使用微軟的Visual Studio開發工具撰寫LINQ程式之外,還有一個好用的工具:「LINQPad」可以協助LINQ查詢的撰寫,此工具可以從「http://www.linqpad.net/」網站中下載:

clip_image002

圖 1:下載「LINQPad」工具。

舉例來說,從網站下載「LINQPad5Setup.exe」接著執行安裝程式,按「下一步」按鈕進行到下一個步驟,請參考下圖所示:

clip_image004

圖 2:安裝「LINQPad」工具。

選取安裝資料夾,然後按「下一步」按鈕進行到下一個步驟,請參考下圖所示:

clip_image006

圖 3:安裝「LINQPad」工具。

選擇是否建立桌面圖示等設定,然後按「下一步」按鈕進行到下一個步驟,請參考下圖所示:

clip_image008

圖 4:安裝「LINQPad」工具。

按「Next」按鈕,進到下一個步驟,然後按下「Install」按鈕安裝,完成後可以看到以下畫面:

clip_image010

圖 5:安裝「LINQPad」工具。

執行安裝完的LINQPad工具後,我們可以將上述範例直接輸入畫面中的「Query」視窗,然後按下執行按鈕,就可以直接測試程式,請參考下圖所示:

clip_image012

圖 6:在「LINQPad」工具執行LINQ查詢。

 

標準查詢運算子(Standard Query Operator)

LINQ中的標準查詢運算子(Standard Query Operator),實際上是IEnumerable<T>或IQueryable<T>型別的擴充方法。例如前文範例中的「where」與「select」:

 

var allCourses = from item in courses
                 where item.StartsWith("C")
                 select item;

實際上會在編譯階段轉換為擴充方法語法,因此對於查詢效能而言兩種寫法是沒有差別的。

 

Where 查詢運算子

在一個LINQ查詢中,「where」運算子可以出現多次,例如以下範例先使用「where」篩選出「Duration」大於「10」的課程資料,然後再叫用「where」篩選「Programming」類型的課程:

using System;
using System.Linq;

namespace LINQDemo {
  enum ClassType {
    Programming,
    Management,
    Database
  }

  class Course {
    public int CourseId { get; set; }
    public string Title { get; set; }
    public int Duration { get; set; }
    public ClassType Type { get; set; }

  }
  class Program {
    static void Main( string [] args ) {
      Course [] courses = new Course [] {
               new Course () {
                  CourseId = 1 ,
                  Title = "C#" ,
                  Duration = 10,
                  Type  = ClassType.Programming
               },
               new Course () {
                  CourseId = 2 ,
                  Title = "SQL Server" ,
                  Duration = 15,
                  Type  = ClassType.Database
               },
               new Course () {
                  CourseId = 3 ,
                  Title = "Visual Basic" ,
                  Duration = 12,
                  Type  = ClassType.Programming
               },
               new Course () {
                  CourseId = 4 ,
                  Title = "JavaScript" ,
                  Duration = 20,
                  Type  = ClassType.Programming
               },
               new Course () {
                  CourseId = 5 ,
                  Title = "PMP" ,
                  Duration = 20,
                  Type  =ClassType.Management
               }
      };

      var allCourses = from item in courses
                       where item.Duration > 10
                       where item.Type == ClassType.Programming
                       select item;

      foreach ( var item in allCourses ) {
         Console.WriteLine( $" {item.CourseId} - {item.Title} - {item.Duration} - {item.Type}" );
      }
      Console.ReadLine( );
    }
  }
}

 

此範例執行結果印出以下資料:

3 - Visual Basic - 12 - Programming
4 - JavaScript - 20 - Programming

若改用擴充方法語法,程式碼參考如下:

var allCourses = courses.Where( item => item.Duration > 10 && item.Type == ClassType.Programming );

也可以將程式碼撰寫如下,重複叫用兩次「where」來設定多個篩選條件:

var allCourses = courses.Where( item => item.Duration > 10 )
.Where( item => item.Type == ClassType.Programming );

OrderBy 與OrderByDescending查詢運算子

「OrderBy」查詢運算子會將資料由小到大排序;而「OrderByDescending」則是由大到小排序。參考以下範例程式碼,根據「CourseId」做升冪排序:

var allCourses = from item in courses
orderby item.CourseId
select item;

若改用擴充方法語法,程式碼參考如下:

var allCourses = courses.OrderBy( item => item.CourseId );

此範例執行結果印出以下資料:

1 - C# - 10 - Programming
2 - SQL Server - 15 - Database
3 - Visual Basic - 12 - Programming
4 - JavaScript - 20 - Programming
5 - PMP - 20 – Management

參考以下範例程式碼,根據「CourseId」做降冪排序:

var allCourses = from item in courses
orderby item.CourseId descending
select item;

若改用擴充方法語法,程式碼參考如下:

var allCourses = courses.OrderByDescending( item => item.CourseId );

此範例執行結果印出以下資料:

5 - PMP - 20 - Management
4 - JavaScript - 20 - Programming
3 - Visual Basic - 12 - Programming
2 - SQL Server - 15 - Database
1 - C# - 10 - Programming

讓我們試著改寫程式碼如下,若根據Course物件做排序:

var allCourses = from item in courses
orderby item
select item;

這次執行會得到一個例外錯誤訊息,如下:

Unhandled Exception: System.ArgumentException: At least one object must implement IComparable.

錯誤訊息請參考下圖所示:

clip_image014

圖 7:排序例外。

這是因為「Course」類別並未實作「IComparable」介面,無法知道如何比大、小。若利用Visual Studio 做開發,可以透過工具的輔助來實做介面。先修改「Course」類別程式碼,使其實作「IComparable」介面,然後將滑鼠移動到「IComparable」介面上方,此時Visual Studio會自動出現一個燈泡圖示(Quick Acton),詢問要如何實作介面,請參考下圖所示:

clip_image016

圖 8:實作「IComparable」介面。

我們希望根據「CourseId」的值來排序,因此選取上圖的「Implement interface through ‘CourseId’」項目,此時Visual Studio工具將自動產生「CompareTo」方法程式碼如下:

class Course : IComparable {
  public int CourseId { get; set; }
  public string Title { get; set; }
  public int Duration { get; set; }
  public ClassType Type { get; set; }

  public int CompareTo( object obj ) {
    return CourseId.CompareTo( obj );
  }
}

修改「CompareTo」方法方法程式碼如下:

class Course : IComparable {
  public int CourseId { get; set; }
  public string Title { get; set; }
  public int Duration { get; set; }
  public ClassType Type { get; set; }

  public int CompareTo( object obj ) {
    return CourseId.CompareTo( ( (Course) obj ).CourseId );
  }
}

這樣才能順利執行查詢。

 

多屬性欄位排序與ThenBy與ThenByDecending

若要根據多個屬性欄位做排序,可以在「orderBy」 運算子後方使用「,」號區隔屬性欄位,例如以下範例程式碼先根據「Duration」再根據「CourseId」進行降冪排序:

var allCourses = from item in courses
orderby item.Duration, item.CourseId descending
select item;

若要在擴充方法查詢根據兩個以上的屬性欄位做排序,可以叫用「ThenBy」或「ThenByDecending」擴充方法,上例程式可以修改如下:

var allCourses = courses.OrderBy( item => item.Duration ).ThenByDescending( item => item.CourseId );

此範例執行結果印出以下資料:

1 - C# - 10 - Programming
3 - Visual Basic - 12 - Programming
2 - SQL Server - 15 - Database
5 - PMP - 20 - Management
4 - JavaScript - 20 – Programming

以下範例程式碼先根據「Duration」再根據「CourseId」進行升冪排序

var allCourses = from item in courses
orderby item.Duration, item.CourseId ascending
select item;

若改用擴充方法語法,程式碼參考如下:

var allCourses = courses.OrderBy( item => item.Duration ).ThenBy( item => item.CourseId );

此範例執行結果印出以下資料:

1 - C# - 10 - Programming
3 - Visual Basic - 12 - Programming
2 - SQL Server - 15 - Database
4 - JavaScript - 20 - Programming
5 - PMP - 20 – Management

 

GroupBy查詢運算子

「GroupBy」查詢運算子可以根據指定的「key」值將物件作分組。分組資料將放在實作IGrouping<TKey,TSource>介面的物件中,TKey表示「key」值;TSource則是滿足分組條件的物件。以下程式碼根據課程的類型(Type)作分組,每一個分組中包含一個集合存放此隸屬於此群組的Course資料:

var allGroups = from item in courses
                group item by item.Type;

foreach ( var group in allGroups ) {
  Console.WriteLine( $" Group : {group.Key} " );
  foreach ( var c in group ) {
    Console.WriteLine( $" \t {c.CourseId} - {c.Title} - {c.Duration} - {c.Type}" );
  }
}

若改用擴充方法語法,程式碼參考如下:

var allGroups = courses.GroupBy( item => item.Type );

這個範例程式的執行結果參考如下:

Group : Programming
1 - C# - 10 - Programming
3 - Visual Basic - 12 - Programming
4 - JavaScript - 20 – - Programming
Group : Database
2 - SQL Server - 15 - Database
Group : Management
5 - PMP - 20 – Management

Viewing all 73 articles
Browse latest View live