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

LINQ語法簡介 - 2

$
0
0

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

在這篇文章中,將延續《LINQ語法簡介 - 1》一文的情境,介紹常用的LINQ運算子(Operator),以透過更簡易的語法來查詢陣列或集合中的內容。

 

Select運算子

LINQ查詢運算式的語法,通常以「select」或「groupby」關鍵字結束,「select」運算子會回傳IEnumerable<T>集合,集合中的項目包含的值則來自於轉換程式。以下程式碼範例利用「select」運算子回傳「customers」集合中「Customer」物件的「CustomerName」屬性值:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
    public int CustomerTypeId { get; set; }

  }
  class Progarm {
    static void Main( string [] args ) {

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerTypeId = 1 , CustomerName ="Mary " , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerTypeId = 1 , CustomerName ="Ana " , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerTypeId = 2 , CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" },
        new Customer(){ CustomerID = 4, CustomerTypeId = 3 ,  CustomerName ="Betty" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "US" }
      };

      var result = from c in customers
                   select c.CustomerName;

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

    }
  }

}

 

此範例執行結果參考如下:

Mary
Ana
Lili
Betty

若改成擴充方法語法,則寫法如下:

var result = customers.Select( c => c.CustomerName );

 

回傳匿名型別

而以下範例程式則利用「select」運算子回傳一個匿名型別所成的集合,此匿名型別包含「CustomerID」、「CustomerName」與「ContactInfo」三個屬性,而「ContactInfo」屬性值是由Customer物件的「ContactName」、「City」與「Country」三個屬性組成:

var result = from c in customers
              select new {
                c.CustomerID ,
                c.CustomerName,
                ContactInfo = $"{ c.ContactName} - {c.City} - {c.Country}"
              };

foreach ( var item in result ) {
   Console.WriteLine( $" {item.CustomerID} , {item.CustomerName} , {item.ContactInfo}" );
}

 

這個範例執行結果參考如下:

1 , Mary , Maria Anders - Berlin - Germany
2 , Ana , Ana Trujillo - Mexico - Mexico
3 , Lili , Futterkiste - Mexico - UK
4 , Betty , Futterkiste - Mexico - US

若改成擴充方法語法,則寫法如下:

var result = customers.Select( c => new {
  c.CustomerID ,
  c.CustomerName ,
  ContactInfo = $"{ c.ContactName} - {c.City} - {c.Country}"
} );

 

 

SelectMany運算子

有時在查詢運算式中我們可能會使用到兩個以上的「from」運算子,例如以下範例程式碼,第一個「from」從「customers」集合中找出所有的顧客(Customer),而第二個「from」則從每一個「Customer」物件的「Orders」屬性找出所有的訂單(Order)資料,篩選出「EmployeeID」為「2」的訂單資料後,並回傳這個「Order」物件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Order {

    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
  }
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public List<Order> Orders { get; set; }
  }
  class Progarm {
    static void Main( string [] args ) {

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary " ,
          Orders = new List<Order> {
          new Order(){ OrderID = 10001 ,  EmployeeID = 1 , OrderDate = new DateTime(2018,9,18) },
          new Order(){ OrderID = 10002 ,  EmployeeID = 2 , OrderDate = new DateTime(2018,9,19) }
        }
        },
         new Customer(){ CustomerID = 2, CustomerName ="Ana " ,
          Orders = new List<Order> {
          new Order(){ OrderID = 10003 , EmployeeID = 2, OrderDate = new DateTime( 2018 , 9 , 20 ) } ,
          new Order(){ OrderID = 10004 ,  EmployeeID = 3 , OrderDate = new DateTime( 2018 , 9 , 20 )}
        }
    } };

      var result = from c in customers
                   from o in c.Orders
                   where o.EmployeeID == 2
                   select o;


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

    }
  }

}

 

此範例執行結果參考如下:

10002
10003

若改成擴充方法語法,則寫法如下,使用SelectMany運算子:

var result = customers.SelectMany( c => c.Orders ).Where( o => o.EmployeeID == 2 );

若除了查出訂單編號之外,還想要得到顧客的名稱、以及訂單的日期等資訊,此時便可以使用以下程式碼,回傳一個匿名型別:

var result = from c in customers
             from o in c.Orders
             where o.EmployeeID == 2
             select new { o.OrderID , c.CustomerName , o.OrderDate , o.EmployeeID };

foreach ( var item in result ) {
  Console.WriteLine( $"{ item.OrderID} - {item.CustomerName} - {item.OrderDate.ToShortDateString()}" );
}

 

此範例執行結果參考如下:

10002 - Mary - 9/19/2018
10003 - Ana - 9/20/2018

若改成擴充方法語法,則寫法如下:

var result = customers.SelectMany( c => c.Orders ,
( c , o ) => new { o.OrderID , c.CustomerName , o.OrderDate , o.EmployeeID } )
.Where( o => o.EmployeeID == 2 );

 

內部連接Join運算子

LINQ包含類似SQL的內部連接(Inner Join)語法,可以將兩個不同集合(或稱序列)中,鍵值(key)相符的項目找出並回傳。

參考以下範例程式碼,「Customer」代表顧客資料,「Order」則描述訂單資料,一個「Customer」物件可能會有零到多個相關的「Order」物件。範例一開始建立「customers」、「orders」兩個集合,「join」運算子找出外部序列(customers集合)與內部序列(orders集合)中「CustomerID」欄位值相符的資料找出:

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

namespace LINQDemo {
  class Order {

    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
    public int ShipperID { get; set; }
  }

  class Customer {

    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }

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

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary " , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ana " , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" },
        new Customer(){ CustomerID = 4, CustomerName ="Betty" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "US" }

      };

      List<Order> orders = new List<Order> {
        new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) ,ShipperID = 3 },
        new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) ,ShipperID = 1},
        new Order(){ OrderID = 10003 , CustomerID = 3 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) ,ShipperID = 2 }
      };


      var innerJoin = from c in customers // 外部序列 (集合)
                      join o in orders //內部序列 (集合)
                      on c.CustomerID equals o.CustomerID //外部key equaqls 內部key
                      select new {
                        CustomerID = c.CustomerID ,
                        CustomerName = c.CustomerName ,
                        EmployeeID = o.EmployeeID ,
                        ShipperID = o.ShipperID ,
                        OrderDate = o.OrderDate
                      };

      foreach ( var item in innerJoin ) {
        Console.WriteLine( $@" {item.CustomerID} - {item.CustomerName}
        - EmployeeID : {item.EmployeeID}
        - ShipperID : {item.ShipperID}
        - OrderDate :{item.OrderDate.ToShortDateString( )}" );
      }

    }
  }
}

 

範例中「from.. in」後頭接外部序列(集合);「join in」後頭接內部序列(集合),「on」關鍵字後頭用來設定(Key Selector),語法是「外部key equaqls 內部key」,不可以使用C#「==」運算子來比對。這個範例的執行的結果請參考下列所示:

2 - Ana

- EmployeeID : 7

- ShipperID : 3

- OrderDate :9/18/2018

2 - Ana

- EmployeeID : 3

- ShipperID : 1

- OrderDate :9/19/2018

3 - Lili

- EmployeeID : 8

- ShipperID : 2

- OrderDate :9/20/2018

若改成擴充方法語法,則寫法如下:

var innerJoin = customers.Join( orders ,
   c => c.CustomerID ,
   o => o.CustomerID ,
   ( c , o ) => new {
     CustomerID = c.CustomerID ,
     CustomerName = c.CustomerName ,
     EmployeeID = o.EmployeeID ,
     ShipperID = o.ShipperID ,
     OrderDate = o.OrderDate
   }
   );

 

同樣的「customers」為外部序列(集合);「orders」為內部序列(集合)。「join」方法的第一個參數是內部序列;第二個參數是「外部Key Selector」;第三個參數是「內部Key Selector」;最後一個參數是「Result Selector」。

分組連接 - GroupJoin運算子

「GroupJoin」運算子和「join」運算子非常類似,執行連接查詢,和「join」運算子不同的地方在於「GroupJoin」運算子會根據特定的群組鍵值(Group Key)回傳群組資料。「GroupJoin」運算子可以將兩個不同集合(或稱序列)中,鍵值(key)相符的項目找出並回傳分組資料與鍵值。

參考以下範例程式碼,「CustomerType」類別用來描述顧客(Customer)的類型,「Customer」類別則包含一個「CustomerTypeId」的屬性對應到「CustomerType」類別:

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

namespace LINQDemo {
  class CustomerType {
    public int CustomerTypeId { get; set; }
    public string CustomerTypeName { get; set; }
    public string Note { get; set; }

  }
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
    public int CustomerTypeId { get; set; }
  }
  class Progarm {
    static void Main( string [] args ) {

      List<CustomerType> customerTypes = new List<CustomerType> {
        new CustomerType(){ CustomerTypeId = 0 , CustomerTypeName = "Golden" },
        new CustomerType(){ CustomerTypeId = 1 , CustomerTypeName = "Newbie" },
        new CustomerType(){ CustomerTypeId = 2 , CustomerTypeName = "Junior"},
        new CustomerType(){ CustomerTypeId = 3 , CustomerTypeName = "Senior"  }
      };

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerTypeId = 1 , CustomerName ="Mary " , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerTypeId = 1 , CustomerName ="Ana " , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerTypeId = 2 , CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" },
        new Customer(){ CustomerID = 4, CustomerTypeId = 3 ,  CustomerName ="Betty" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "US" }

      };

      var groups = from t in customerTypes // 外部序列 (集合)
                   join c in customers // 內部序列 (集合)
                   on t.CustomerTypeId equals c.CustomerTypeId into customerGroups
                   select new { // result selector
                     CustomerGroups = customerGroups ,
                     CustomerTypeId = t.CustomerTypeId ,
                     CustomerTypeName = t.CustomerTypeName
                   };

      foreach ( var g in groups ) {
        Console.WriteLine( $"Customer Types: {g.CustomerTypeId} - {g.CustomerTypeName}" );
        foreach ( var item in g.CustomerGroups ) {
          Console.WriteLine( $"\t Customer : {item.CustomerID} - {item.CustomerName} - {item.Country} " );

        }
      }

    }
  }

}

 

範例中「from .. in」後頭接外部序列(集合);「join .. in」後頭接內部序列(集合),「on」關鍵字後頭用來設定Key Selector,語法是「外部key equaqls 內部key」,不可以使用C#「==」運算子來比對。最後使用「into」建立分組集合,這個範例的執行的結果請參考下列所示:

Customer Types: 0 - Golden

Customer Types: 1 - Newbie

Customer : 1 - Mary - Germany

Customer : 2 - Ana - Mexico

Customer Types: 2 - Junior

Customer : 3 - Lili - UK

Customer Types: 3 - Senior

Customer : 4 - Betty – US

若改成擴充方法語法,則寫法如下:

var groups = customerTypes.GroupJoin( customers ,
  t => t.CustomerTypeId ,
  c => c.CustomerTypeId ,
  ( t , customerGroups ) => new {
    CustomerGroups = customerGroups ,
    CustomerTypeName = t.CustomerTypeName ,
    CustomerTypeId = t.CustomerTypeId
  } );

 

此範例中「customerTypes」為外部序列(集合),「GroupJoin」方法的第一個參數「customers」為內部序列(集合),第二個參數是「外部Key Selector」;第三個參數是「內部Key Selector」;最後一個參數是「Result Selector」。

Quantifier 運算子 - All

LINQ查詢運算式語法目前不支援All運算子。你可以叫用All方法檢查集合中的項目是否全部滿足條件,若是,則All方法將回傳「True」;否則則回傳「False」。參考以下範例程式碼,檢查所有「CustomerID」為「2」,且訂單(Order)的月份是否為「9」月:

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

namespace LINQDemo {
  class Order {
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
    public int ShipperID { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      List<Order> orders = new List<Order> {
        new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) ,ShipperID = 3 },
        new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) ,ShipperID = 1},
        new Order(){ OrderID = 10003 , CustomerID = 3 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) ,ShipperID = 2 }
      };

      var result = orders.All( o => o.CustomerID == 2 && o.OrderDate.Month == 9 );
      Console.WriteLine( result);
    }
  }
}

 

當然此執行結果為「False」;若修改所有 「Order」物件的「CustomerID」屬性值為「2」,執行結果就會回傳「True」:

List<Order> orders = new List<Order> {

new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) ,ShipperID = 3 },

new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) ,ShipperID = 1},

new Order(){ OrderID = 10003 , CustomerID = 2 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) ,ShipperID = 2 }

};

var result = orders.All( o => o.CustomerID == 2 && o.OrderDate.Month == 9 );
Console.WriteLine( result);

 

Quantifier 運算子 - Any

「Any」運算子檢查集合中的項目只要有一個項目滿足條件,則「Any」方法將回傳「True」;若所有項目都不滿足條件才會回傳「False」。LINQ查詢運算式語法目前不支援「Any」運算子。參考以下範例程式碼,檢查是否有「CustomerID」為「2」,且訂單(Order)的月份是「9」月的訂單資料:

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

namespace LINQDemo {
  class Order {
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
    public int ShipperID { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      List<Order> orders = new List<Order> {
        new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) ,ShipperID = 3 },
        new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) ,ShipperID = 1},
        new Order(){ OrderID = 10003 , CustomerID = 3 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) ,ShipperID = 2 }
      };

      var result = orders.Any( o => o.CustomerID == 2 && o.OrderDate.Month == 9 );
      Console.WriteLine( result);
    }
  }
}

這個範例的結果將會是「true」。


LINQ語法簡介 - 3

$
0
0

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

在這篇文章中,將延續《LINQ語法簡介 - 1》與《LINQ語法簡介 - 2》文章的情境,介紹常用的LINQ運算子(Operator),以透過更簡易的語法來查詢陣列或集合中的內容。

 

Aggregation 運算子 - Aggregate

「Aggregate」運算子用於執行累積運算。例如我們想要撰寫程式碼,計算出「1+2+3+4+5」數學式的總合,可以使用以下程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Aggregate( ( i , j ) => i + j );
Console.WriteLine( $"Result : {result}" ); // Result : 15

讓我們換個寫法來研究一下「Aggregate」的運作,修改程式碼如下,將每次執行匿名方法時當下的「i」與「j」值輸出到主控台:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Aggregate( ( i , j ) => {
   Console.WriteLine( $" {i} - {j}" );
   return i + j;
} );
Console.WriteLine( $"Result : {result}" );

 

這個範例程式的執行結果參考如下:

1 - 2
3 - 3
6 - 4
10 - 5
Result : 15

Aggregate方法一開始,會先將集合中的前兩個項目取出,將第一個項目「1」代入 「i」;第二個項目「2」代入「j」,接著計算「i+j」得到「3」。

再來將上一步驟得到的「3」代入「i」,將集合中下一個項目「3」代入「j」,接著計算「i+j」得到「6」。

再來將上一步驟得到的「6」代入「i」,將集合中下一個項目「4」代入「j」,接著計算「i+j」得到「10」。

依此類推,再來將上一步驟得到的「10」代入「i」,將集合中下一個項目「5」代入「j」,接著計算「i+j」得到「15」。

而以下範例程式碼則是利用「Aggregate」運算子,將集合中「Customer」物件的「CustomerName」串接成按「,」號區隔的字串:

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

namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }

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

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ana" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" },
        new Customer(){ CustomerID = 4, CustomerName ="Betty" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "US" }
      };
      var result = customers.Aggregate<Customer , string>( "Result : " , ( s , c ) => s += c.CustomerName + ",").TrimEnd(',');
      Console.WriteLine(result);
    }
  }
}

 

這個範例程式的執行結果參考如下:

Result : Mary,Ana,Lili,Betty

範例中「Aggregate」方法的第一個參數是初始值(Seed Value);第二個參數是一個Func委派(Delegate),「s」用來放累計運算完的結果,「c」則是代表資料來源的「Customer」物件。在這個範例中叫用了「TrimEnd」方法來去除最後一個「,」號。你也可以直接叫用「Aggregate<Customer , string , string>()」方法,在第三個參數中處理,例如可將上例程式改寫如下,可以得到相同的執行結果:

var result = customers.Aggregate<Customer , string , string>( "Result : " ,
      ( s , c ) => s += c.CustomerName + "," ,
       s => s.TrimEnd( ',' )
      );
Console.WriteLine( result ); //Result : Mary,Ana,Lili,Betty

提示:C# LINQ查詢運算式目前不支援 「Aggreate」語法。

 

Aggregation 運算子 - Average

「Average」運算子可以將陣列或集合中的數值加總後,計算出平均值,參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Average( );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : 3

而以下程式碼範例則是計算出集合中「Order」物件「Amount」屬性的平均值:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Order {
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
    public int Amount { get; set; }
    public int ShipperID { get; internal set; }
  }
  class Program {
    static void Main( string [] args ) {

      List<Order> orders = new List<Order> {
        new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) , ShipperID = 3 , Amount = 5000},
        new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) , ShipperID = 1 , Amount = 4500},
        new Order(){ OrderID = 10003 , CustomerID = 3 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) , ShipperID = 2 , Amount =3000}
      };

      var result = orders.Average( o => o.Amount );
      Console.WriteLine( $"Result : {result}" );

    }
  }
}

 

這個範例程式的執行結果參考如下:

Result : 4166.66666666667

提示:C# LINQ查詢運算式目前不支援 「Average」語法。

 

Aggregation 運算子 - Count

「Count」運算子可以計算出陣列或集合中項目的個數,參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Count( );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : 5

而以下程式碼範例則是計算出集合中「Order」物件「Amount」屬性值大於等於「4000」的項目筆數:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Order {
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
    public int Amount { get; set; }
    public int ShipperID { get; internal set; }
  }
  class Program {
    static void Main( string [] args ) {

      List<Order> orders = new List<Order> {
        new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) , ShipperID = 3 , Amount = 5000},
        new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) , ShipperID = 1 , Amount = 4500},
        new Order(){ OrderID = 10003 , CustomerID = 3 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) , ShipperID = 2 , Amount =3000}
      };

      var result = orders.Count( o => o.Amount >= 4000 );
      Console.WriteLine( $"Result : {result}" );


    }
  }
}

這個範例程式的執行結果參考如下:

Result : 2

提示:C# LINQ查詢運算式目前不支援「Count」語法。

 

Aggregation 運算子 - Max

「Max」運算子用來找出陣列或集合中最大的數值,參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Max( );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : 5

而以下程式碼範例則是計算出集合中「Order」物件「Amount」屬性包含的最大值:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Order {
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
    public int Amount { get; set; }
    public int ShipperID { get; internal set; }
  }
  class Program {
    static void Main( string [] args ) {

      List<Order> orders = new List<Order> {
        new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) , ShipperID = 3 , Amount = 5000},
        new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) , ShipperID = 1 , Amount = 4500},
        new Order(){ OrderID = 10003 , CustomerID = 3 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) , ShipperID = 2 , Amount = 3000}
      };

      var result = orders.Max( o => o.Amount );
      Console.WriteLine( $"Result : {result}" );

    }
  }
}

 

這個範例程式的執行結果參考如下:

Result : 5000

提示:C# LINQ查詢運算式目前不支援 「Max」語法。

 

Aggregation 運算子 - Sum

「Sum」運算子用來加總陣列或集合中的數值,參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Sum( );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : 15

若想要加總陣列或集合中大於等於3的數值,參考以下範例程式碼:

 

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Sum( i => {
  if ( i >= 3 ) {
    return i;
  } else {
    return 0;
    }
} );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : 12

而以下程式碼範例則是計算出集合中「Order」物件「Amount」屬性值的加總:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Order {
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public int EmployeeID { get; set; }
    public DateTime OrderDate { get; set; }
    public int Amount { get; set; }
    public int ShipperID { get; internal set; }
  }
  class Program {
    static void Main( string [] args ) {

      List<Order> orders = new List<Order> {
        new Order(){ OrderID = 10001 , CustomerID = 2 , EmployeeID = 7 , OrderDate = new DateTime(2018,9,18) , ShipperID = 3 , Amount = 5000},
        new Order(){ OrderID = 10002 , CustomerID = 2 , EmployeeID = 3 , OrderDate = new DateTime(2018,9,19) , ShipperID = 1 , Amount = 4500},
        new Order(){ OrderID = 10003 , CustomerID = 3 , EmployeeID = 8 , OrderDate = new DateTime(2018,9,20) , ShipperID = 2 , Amount = 3000}
      };

      var result = orders.Sum( o => o.Amount );
      Console.WriteLine( $"Result : {result}" );


    }
  }
}

 

這個範例程式的執行結果參考如下:

Result : 12500

提示:C# LINQ查詢運算式目前不支援 「Sum」語法。

 

Element運算子 - ElementAt

「ElementAt」運算子回傳陣列或集合中指定索引值(Index)的項目,參考以下範例程式碼,找出索引「3」所在的數值,索引以「0」開始,在以下範例中list索引「0」的項目是「1」;索引「1」的項目是「2」,依此類推:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.ElementAt( 3 );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : 4

若指定的索引超過集合中最大項目的索引,參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.ElementAt( 8 );
Console.WriteLine( $"Result : {result}" );

則執行將產生例外錯誤,請參考下圖所示:

clip_image002

圖 1:索引超過範圍產生例外錯誤。

提示:C# LINQ查詢運算式目前不支援 「ElementAt」語法。

 

Element運算子 - ElementAtOrDefault

「ElementAtOrDefault」運算子回傳陣列或集合中指定索引值(Index)的項目。「ElementAt 」與「ElementAtOrDefault」的差異是:「ElementAt 」找不到條件相符的資料會產生例外錯誤;而「ElementAtOrDefault」找不到條件相符的資料時不會產生例外錯誤,而是回傳「預設值」,參考以下範例程式碼,找出索引「3」所在的數值:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.ElementAtOrDefault( 3 );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : 4

若指定的索引超過集合中最大項目的索引,參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.ElementAtOrDefault( 8 );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下,回傳預設值「0」:

Result : 0

而以下範例從字串集合中找索引「8」的項目,若索引超過陣列或集合中最大項目的索引,「ElementAtOrDefault」方法則回傳字串的預設值「null」:

List<string> list = new List<string>( ) { "1" , "2" , "3" , "4" , "5" };
var result = list.ElementAtOrDefault( 8 );
Console.WriteLine(result == null); // true

提示:C# LINQ查詢運算式目前不支援 「ElementAtOrDefault」語法。

 

Element運算子 – First與FirstOrDefault

「First」與「FirstOrDefault」運算子用來取得陣列或集合中的第一個項目。「First」與「FirstOrDefault」運算子的差異是:「First」方法找不到條件相符的資料會產生例外錯誤;而「FirstOrDefault」方法找不到條件相符的資料時不會產生例外錯誤,而是回傳預設值。參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.First( );
Console.WriteLine( $"Result : {result}" );
var result2 = list.FirstOrDefault( );
Console.WriteLine( $"Result2 : {result2}" );

這個範例程式的執行結果參考如下:

Result : 1
Result2 : 1

以下範例程式碼將大於3的第一個項目回傳

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.First( i => i > 3 );
Console.WriteLine( $"Result : {result}" );
var result2 = list.FirstOrDefault( i => i > 3 );
Console.WriteLine( $"Result2 : {result2}" );

這個範例程式的執行結果參考如下:

Result2 : 4
Result : 4

提示:C# LINQ查詢運算式目前不支援 「First」與「FirstOrDefault」語法。

 

Element運算子 – Last與LastOrDefault

「Last」與「LastOrDefault」運算子用來取得陣列或集合中的最後一個項目。「Last」與「LastOrDefault」運算子的差異是:「Last」方法找不到條件相符的資料會產生例外錯誤;而「LastOrDefault」方法找不到條件相符的資料時不會產生例外錯誤,而是回傳預設值。參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Last( );
Console.WriteLine( $"Result : {result}" );
var result2 = list.FirstOrDefault( );
Console.WriteLine( $"Result2 : {result2}" );

這個範例程式的執行結果參考如下:

Result : 5
Result2 : 5

以下範例程式碼小於3的最後一個項目回傳

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Last( i => i < 3 );
Console.WriteLine( $"Result : {result}" );
var result2 = list. LastOrDefault( i => i < 3 );
Console.WriteLine( $"Result2 : {result2}" );

這個範例程式的執行結果參考如下:

Result2 : 2
Result : 2

提示:C# LINQ查詢運算式目前不支援 「Last」與「LastOrDefault」語法。

Element運算子 – Single與SingleOrDefault

「Single」與「SingleOrDefault」運算子用來取得陣列或集合中唯一的一個項目。「Single」與「SingleOrDefault」運算子的差異是:「Single」方法找不到條件相符的資料會產生例外錯誤;而「SingleOrDefault」方法找不到條件相符的資料時不會產生例外錯誤,而是回傳預設值。參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 };
var result = list.Single( );
Console.WriteLine( $"Result : {result}" );
var result2 = list.SingleOrDefault( );
Console.WriteLine( $"Result2 : {result2}" );

這個範例程式的執行結果參考如下:

Result : 1
Result2 : 1

若來源集合包含兩個項目,則「Single」與「SingleOrDefault」都會產生例外錯誤:

List<int> list = new List<int>( ) { 1 , 2 };
var result = list.Single( ); // Exception
Console.WriteLine( $"Result : {result}" );

var result2 = list.SingleOrDefault( ); // Exception
Console.WriteLine( $"Result2 : {result2}" );

若來源集合沒有任何項目,則「Single」會產生例外錯誤,「SingleOrDefault」會回傳預設值「0」:

List<int> list = new List<int>( ) { };
var result = list.Single( ); // Exception
Console.WriteLine( $"Result : {result}" );

var result2 = list.SingleOrDefault( ); // 預設值 Result2 : 0
Console.WriteLine( $"Result2 : {result2}" );

以下範例程式碼範例將大於「4」的唯一一個項目回傳:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Single( i => i > 4 );
Console.WriteLine( $"Result : {result}" );
var result2 = list.SingleOrDefault( i => i > 4 );
Console.WriteLine( $"Result2 : {result2}" );

這個範例程式的執行結果參考如下:

Result2 :5
Result : 5

若沒有找到相符的唯一一個項目,「Single」方法將產生例外;「SingleOrDefault」則回傳預設值,參考以下範例程式碼:

List<int> list = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list.Single( i => i == 6 ); //Exception
Console.WriteLine( $"Result : {result}" );

var result2 = list.SingleOrDefault( i => i == 6 );
Console.WriteLine( $"Result2 : {result2}" ); //Result2 : 0

提示:C# LINQ查詢運算式目前不支援 「Single」與「SingleOrDefault」語法。

LINQ語法簡介 - 4

$
0
0

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

在這篇文章中,將延續《LINQ語法簡介 - 1》、《LINQ語法簡介 - 2》與《LINQ語法簡介 - 3》文章的情境,介紹常用的LINQ運算子(Operator),以透過更簡易的語法來查詢陣列或集合中的內容。

 

比較運算子 - SequenceEqual

「SequenceEqual」是唯一的一個比較運算子,對於基礎資料型別(Primitive Data Types)而言,「SequenceEqual」運算子比較陣列或集合中的每一個項目的個數與值是否完全相同,若完全相同則回傳「True」,否則則回傳「False」,參考以下範例程式碼:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
List<int> list2 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list1.SequenceEqual( list2 );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : True

只要其中有一個項目不同,比較就不相等,參考以下範例程式碼,集合中第一、二項目的值不同:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
List<int> list2 = new List<int>( ) { 2 , 1 , 3 , 4 , 5 };
var result = list1.SequenceEqual( list2 );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : False

同樣地,集合中項目個數不同,比較就不相等,參考以下範例程式碼:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
List<int> list2 = new List<int>( ) { 1 , 2 , 3 , 4 };
var result = list1.SequenceEqual( list2 );
Console.WriteLine( $"Result : {result}" );

這個範例程式的執行結果參考如下:

Result : False

提示:C# LINQ查詢運算式目前不支援 「SequenceEqual」語法。

 

複雜型別比較

對於複雜型別來說,「SequenceEqual」運算子檢查兩個物件的參考來判斷兩個序列是否相等。例如以下範例程式碼,「customers1」、「customers2」兩個集合中存放相同的「Customer」物件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {

      var customer = new Customer( ) { CustomerID = 1 , CustomerName = "Mary " , ContactName = "Maria Anders" , City = "Berlin" , PostalCode = 12209 , Country = "Germany" };

      List<Customer> customers1 = new List<Customer> { customer };
      List<Customer> customers2 = new List<Customer> { customer };

      var result = customers1.SequenceEqual( customers2 );
      Console.WriteLine( $"Result : {result}" );
    }
  }
}

這個範例程式的執行結果參考如下:

Result : True

若程式改寫如下,兩集合中各新增一個屬性值完全相同的「Customer」物件:

List<Customer> customers1 = new List<Customer> { new Customer( ) { CustomerID = 1 , CustomerName = "Mary " , ContactName = "Maria Anders" , City = "Berlin" , PostalCode = 12209 , Country = "Germany" }};
List<Customer> customers2 = new List<Customer> { new Customer( ) { CustomerID = 1 , CustomerName = "Mary " , ContactName = "Maria Anders" , City = "Berlin" , PostalCode = 12209 , Country = "Germany" }};
var result = customers1.SequenceEqual( customers2 );
Console.WriteLine( $"Result : {result}" );

 

因為比較的是物件的參考,所以這個範例的執行結果會回傳「false」:

Result : False

我們可以自訂一個類別,實作「IEqualityComparer<T>」介面來自訂比較的規則,參考以下範例程式碼,「MyComparer」類別實作「IEqualityComparer< Customer >」介面,並改寫「Equals」方法,在兩個序列中當「Customer」物件之「CustomerID」與「CustomerName」屬性值相同時,將兩序列視為相等。接著在叫用「SequenceEqual」方法時,傳入「MyComparer」實體:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class MyComparer : IEqualityComparer<Customer> {
    public bool Equals( Customer x , Customer y ) {
      if ( x.CustomerID == y.CustomerID && x.CustomerName == y.CustomerName ) {
        return true;
      } else {
        return false;
      }
    }
    public int GetHashCode( Customer obj ) => obj.GetHashCode( );
  }
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      List<Customer> customers1 = new List<Customer> { new Customer( ) { CustomerID = 1 , CustomerName = "Mary " , ContactName = "Maria Anders" , City = "Berlin" , PostalCode = 12209 , Country = "Germany" } };
      List<Customer> customers2 = new List<Customer> { new Customer( ) { CustomerID = 1 , CustomerName = "Mary " , ContactName = "Maria Anders" , City = "Berlin" , PostalCode = 12209 , Country = "Germany" } };
      var result = customers1.SequenceEqual( customers2 , new MyComparer( ) );
      Console.WriteLine( $"Result : {result}" );
    }
  }
}

 

這個範例程式的執行結果參考如下:

Result : True

 

Concat運算子

「Concat」運算子可以將兩個序列合併在一起,並回傳一個新的序列。參考以下範例程式碼:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 };
List<int> list2 = new List<int>( ) { 4 , 5 , 6 };
var result = list1.Concat( list2 );

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


這個範例程式的執行結果參考如下:

1
2
3
4
5
6

提示:C# LINQ查詢運算式目前不支援 「Concat」語法。

 

Zip運算子

「Zip」運算子類似「Concat」運算子可以將兩個序列合併在一起,「Zip」運算子利用一個Func委派進行一些計算,並回傳一個新的序列。參考以下範例程式碼,將兩個集合的項目,按順序相加:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 };
List<int> list2 = new List<int>( ) { 4 , 5 , 6 };
var result = list1.Zip( list2 , ( i , j ) => i + j );

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

 

這個範例程式的執行結果參考如下:

5
7
9

提示:C# LINQ查詢運算式目前不支援 「Zip」語法。

 

Generation運算子 - DefaultIfEmpty

「DefaultIfEmpty」會回傳一個新的集合,若原始集合或陣列不是空集合(空陣列),則「DefaultIfEmpty」就會將原集合或陣列中的資料,複製到新集合或陣列新陣列中;若原始原始集合或陣列沒有任何項目,則新集合會包含一個項目,項目的預設值則根據原始集合或陣列的型別決定。

參考以下範例程式碼,若原始集合不是空集合,則「DefaultIfEmpty」就會將原集合中的資料,複製到新集合中:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 };
var result = list1.DefaultIfEmpty( );
Console.WriteLine( $"Count : {result.Count( )}" ); //3
foreach ( var item in list1 ) {
   Console.WriteLine(item);
}

 

這個範例程式的執行結果參考如下:

Count : 3
1
2
3

若原始集合的型別為「string」型別,則「DefaultIfEmpty」在空集合的情況下,回傳的新集合第一個項目的預設值為「null」,參考以下範例程式碼:

List<string> list1 = new List<string>( );
var result = list1.DefaultIfEmpty( );
Console.WriteLine( $"Count : {result.Count( )}" ); //1
Console.WriteLine( result.First( ) == null ); // True

這個範例程式的執行結果參考如下:

Count : 1
True

若原始集合的型別為「int」型別,則「DefaultIfEmpty」在空集合的情況下,回傳的新集合第一個項目的預設值為「0」:

List<int> list1 = new List<int>( );
var result = list1.DefaultIfEmpty( );
Console.WriteLine( $"Count : {result.Count( )}" ); //1
Console.WriteLine( result.First( ) ); // 0

「DefaultIfEmpty」還可以指定要回傳的預設值,參考以下範例程式碼,當原始集合的型別為「int」型別,則「DefaultIfEmpty」在空集合的情況下,設定回傳的新集合第一個項目的預設值為「100」:

List<int> list1 = new List<int>( );
var result = list1.DefaultIfEmpty( 100 );
Console.WriteLine( $"Count : {result.Count( )}" ); //1
Console.WriteLine( result.First( ) ); // 100

這個範例程式的執行結果參考如下:

Count : 1
100

提示:C# LINQ查詢運算式目前不支援 「DefaultIfEmpty」語法。

 

Generation運算子 - Empty

「Empty」運算子用來回傳一個空的陣列或集合,參考以下範例程式碼,回傳一個空白的字串陣列:

var list = Enumerable.Empty<string>( );
Console.WriteLine( $"Count : {list.Count( )}" ); // Count : 0
Console.WriteLine( $"Type : {list.GetType()}" ); // Type : System.String[]
提示:C# LINQ查詢運算式目前不支援 「Empty」語法。

 

Generation運算子 - Range

「Range」方法會產生指定個數的項目,放到一個集合中回傳,參考以下範例程式碼,叫用「Range」方法產生一個集合,存放1到10數值的項目。:

var list = Enumerable.Range( 1 , 10 );
foreach ( var item in list ) {
  Console.WriteLine(item);
}

「Range」方法的第一個參數用來指定初始值,第二個參數是要產生的項目個數。這個範例程式的執行結果參考如下:

clip_image002

圖 1:Range。

Generation運算子 - Repeat

「Repeat」方法用來產生重複的項目,參考以下範例程式碼,重複產生數值「1」五次:

var list = Enumerable.Repeat( 1 , 5 );
foreach ( var item in list ) {
  Console.WriteLine(item);
}

這個範例程式的執行結果參考如下:

clip_image004

圖 2:Repeat。

Set 運算子 - Distinct

「Distinct」用來找出陣列或集合中唯一值,參考以下範例程式碼:

int [] list = new int [] { 10 , 1 , 33 , 10 , 1 , 5 };
foreach ( var item in list.Distinct() ) {
  Console.WriteLine(item);
}

這個範例程式的執行結果參考如下:

clip_image006

圖 3:Distinct。

針對複雜型別,預設「Distinct」無法比對唯一性,參考以下範例程式碼,集合中包含多個屬性值完全相同的物件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary " , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 1, CustomerName ="Mary " , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann " , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann " , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };

      foreach ( var item in customers.Distinct( ) ) {
        Console.WriteLine( $" {item.CustomerID} - {item.CustomerName} - {item.ContactName}" );
      }

    }
  }
}

 
這個範例程式的執行結果參考如下,集合中的「Customer」都會被視為唯一的物件:

clip_image008

圖 4:Distinct。

我們需要實作「IEqualityComparer<T>」介面來比較複雜型別,參考以下範例程式碼,在叫用「Distinct」方法時,傳入「MyComparer」物件:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class MyComparer : IEqualityComparer<Customer> {
    public bool Equals( Customer x , Customer y ) {
      if ( x.CustomerID == y.CustomerID && x.CustomerName == y.CustomerName ) {
        return true;
      } else {
        return false;
      }
    }
    public int GetHashCode( Customer obj ) => obj.CustomerID.GetHashCode( );
  }
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };

      foreach ( var item in customers.Distinct( new MyComparer() ) ) {
        Console.WriteLine( $" {item.CustomerID} - {item.CustomerName} - {item.ContactName}" );
      }

    }
  }
}

 

這個範例程式的執行結果參考如下:

clip_image010

圖 5:Distinct。

Set 運算子 - Except

「Except」有減去的效果,從一個集合中,移除和另一個集合重複的內容,參考以下範例程式碼,兩個集合中都出現「4」、「5」兩個項目:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
List<int> list2 = new List<int>( ) { 4 , 5 , 6 , 7 , 8 };
var result = list1.Except( list2 );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

這個範例程式的執行結果參考如下,新集合中不包含「4」、「5」兩個項目:

clip_image012

圖 6:Except。

Set 運算子 - Intersect

「Intersect」有取交集的效果,找出兩個集合中重複的內容,參考以下範例程式碼:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
List<int> list2 = new List<int>( ) { 4 , 5 , 6 , 7 , 8 };
var result = list1.Intersect( list2 );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

這個範例程式的執行結果參考如下:

clip_image014

圖 7:Intersect。

Set 運算子 - Union

「Union」有取聯集的效果,找出兩個集合中不重複的內容,參考以下範例程式碼:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
List<int> list2 = new List<int>( ) { 4 , 5 , 6 , 7 , 8 };
var result = list1.Union( list2 );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

這個範例程式的執行結果參考如下:

clip_image016

圖 8:Union。

Partitioning 運算子 - Skip

「Skip」會跳過指定個數的項目,將剩餘的項目放在集合中回傳,參考以下範例程式碼,跳過前兩個項目:

List<int> list1 = new List<int>( ) { 1 , 2 , 3 , 4 , 5 };
var result = list1.Skip( 2 );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

這個範例程式的執行結果參考如下:

clip_image018

圖 9:Skip。

Partitioning 運算子 - SkipWhile

「SkipWhile」可以根據條件來跳過項目,將剩餘的項目放在集合中回傳,參考以下範例程式碼,跳過集合中項目值小於「4」的項目:

List<int> list1 = new List<int>( ) { 1 , 2 , 5 , 4 , 3 };
var result = list1.SkipWhile( i => i < 4 );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

這個範例程式的執行結果參考如下:

clip_image020

圖 10:SkipWhile。

範例中集合的第一、二個項目丟到篩選條件式比對都會得到「true」,因此跳過這兩個項目。「SkipWhile」只要找到一個項目符合條件就會停止跳過的動作,因此這個範例中集合中第三個項目「5」丟到篩選條件式比對 「5 < 4」為「false」,接著就停止比對的動作。

 

Partitioning 運算子 - Take

「Take」從陣列或集合中,從頭開始回傳指定個數的項目,參考以下範例程式碼,取得前三個項目:

List<int> list1 = new List<int>( ) { 1 , 2 , 5 , 4 , 3 };
var result = list1.Take( 3 );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

這個範例程式的執行結果參考如下:

clip_image022

圖 11:Take。

Partitioning 運算子 - TakeWhile

「TakeWhile」從陣列或集合中,將滿足條件的項目回傳,只要找到其中一個項目滿足條件,就停止。參考以下範例程式碼,取得前2個項目:

List<int> list1 = new List<int>( ) { 1 , 2 , 5 , 4 , 3 };
var result = list1.TakeWhile( i => i < 4 );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

這個範例程式的執行結果參考如下:

clip_image024

圖 12:TakeWhile。

LINQ語法簡介 - 5

$
0
0

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

在這篇文章中,將延續《LINQ語法簡介 - 1》、《LINQ語法簡介 - 2》、《LINQ語法簡介 - 3》與《LINQ語法簡介 - 4》文章的情境,介紹常用的LINQ運算子(Operator),以透過更簡易的語法來查詢陣列或集合中的內容,這一篇主要介紹轉換運算子,包含:「ToArray」、「ToList」、「ToDictionary」、「ToLookup」、「Cast」、「AsEnumerable」與「OfType」。

 

轉換運算子 – ToArray

轉型運算子可以根據需求將序列轉換成集合或陣列,「ToArray」方法用來轉換成陣列,參考以下範例程式碼,將「Customer」集合轉換成陣列,然後透過「foreach」迴圈印出陣列中「Customer」物件的客戶名稱「CustomerName」:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {

      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };
      Customer [] customersArray = customers.ToArray( );
      foreach ( var item in customersArray ) {
        Console.WriteLine(item.CustomerName);
      }
    }
  }
}

 

這個範例程式的執行結果參考如下:

clip_image002

圖 1:轉換算子 - 「ToArray」。

轉換運算子 – ToList

「ToList」用來將序列轉換成集合,參考以下範例程式碼,叫用「ToList」方法,將「Customer」陣列轉換成集合:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      Customer [] customers = new Customer [] {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };
      List<Customer> customersArray = customers.ToList( );
      foreach ( var item in customersArray ) {
        Console.WriteLine(item.CustomerName);
      }
    }
  }
}

 

這個範例程式的執行結果參考如下:

clip_image002[1]

圖 2:轉換算子 - 「ToList」。

轉換運算子 – ToDictionary

「ToDictionary」可以將來源序列轉換成Dictionary類型的物件,叫用此方法時,需要利用Func委派指定分組鍵值(Key),參考以下範例程式碼,指定鍵值為「CustomerID」:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };

      IDictionary<int , Customer> dic = customers.ToDictionary( c => c.CustomerID );

      foreach ( var item in dic ) {
        Console.WriteLine( $" {item.Key} - {item.Value.CustomerName}" );
      }
    }
  }
}

 

這個範例程式的執行結果參考如下:

clip_image004

圖 3:轉換算子 - 「ToDictionary」。

轉換運算子 – ToLookup

「ToLookup」的用法和「GroupBy」一樣應用在資料分組,唯一的不同點是「GroupBy」是延遲執行查詢,而「ToLookup」是馬上執行查詢,參考以下範例程式碼,根據「Customer」的「City」做資料分組:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      List<Customer> customers = new List<Customer> {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };

      ILookup<string , Customer> customerGroup = customers.ToLookup( c => c.City );

      foreach ( var g in customerGroup ) {
        Console.WriteLine(g.Key);
        foreach ( var item in g ) {
          Console.WriteLine( $" {item.CustomerName}" );
        }
      }
    }
  }
}

 

 

這個範例程式的執行結果參考如下:

clip_image006

圖 4:轉換算子 - 「ToLookup」。

轉換運算子 – Cast

「Cast」用來將序列轉換成「IEnumerable<T>」型別,參考以下範例程式碼展示如何轉換「Customer」陣列:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      Customer [] customers = new Customer [] {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };

      IEnumerable<Customer> customersArray = customers.Cast<Customer>( );

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

 

這個範例程式的執行結果參考如下:

clip_image007

圖 5:轉換算子 - 「Cast」。

轉換運算子 – AsEnumerable

「AsEnumerable」的用途和「Cast」一樣,用來將序列轉換成「IEnumerable<T>」型別,參考以下範例程式碼展示如何轉換「Customer」陣列:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LINQDemo {
  class Customer {
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public string ContactName { get; set; }
    public string City { get; set; }
    public int PostalCode { get; set; }
    public string Country { get; set; }
  }
  class Program {
    static void Main( string [] args ) {
      Customer [] customers = new Customer [] {
        new Customer(){ CustomerID = 1, CustomerName ="Mary" , ContactName = "Maria Anders" , City = "Berlin", PostalCode = 12209 , Country = "Germany" },
        new Customer(){ CustomerID = 2, CustomerName ="Ann" , ContactName = "Ana Trujillo" , City = "México ", PostalCode = 05021 , Country = "Mexico" },
        new Customer(){ CustomerID = 3, CustomerName ="Lili" , ContactName="Futterkiste" , City = "México ", PostalCode = 05023 , Country = "UK" }
      };

      IEnumerable<Customer> customersArray = customers.AsEnumerable();

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

 

這個範例程式的執行結果參考如下:

clip_image008

圖 6:轉換算子 - 「AsEnumerable」。

轉換運算子 – OfType

「OfType」運算子可以從陣列或集合中,找出指定型別的物件,放到集合中回傳,參考以下範例程式碼,找出集合中所有的字串:

List<object> list = new List<object> { 10 , "hello" , DateTime.Now , true , 'c' , 20 , "world" };
var result = from item in list.OfType<string>( )
select item;

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

這個範例程式的執行結果參考如下:

clip_image010

圖 7:轉換算子 - 「OfType」。

若改成擴充方法語法,則寫法如下:

List<object> list = new List<object> { 10 , "hello" , DateTime.Now , true , 'c' , 20 , "world" };
var result = list.OfType<string>( );
foreach ( var item in result ) {
  Console.WriteLine( item );
}

初探Blazor

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N180719701
出刊日期: 2018/7/11

目前若使用微軟開發技術來寫網站,你至少需要學會兩種程式語言,後端開發利用C#,前端則需撰寫JavaScript。Blazor是一個新的.NET網站框架(.NET web framework),以WebAssembly標準為基礎,可以取代以往使用JavaScript語言,而改用C# / Razor語法、HTML標籤以建立執行在瀏覽器上的用戶端應用程式,有了Blazor就可以讓程式設計師專注在一種程式語言,使用C# 語言進行全端開發(full stack web development)。Blazor是從這句話衍生出來的:

Browser + Razor = Blazor

WebAssembly(簡寫為Wasm)是二進位格式的指令,適用於stack-based 虛擬機器,應用於瀏覽器中,提供比JavaScript更快的編譯與執行速度,能讓開發者使用熟悉的程式語言來設計程式。目前Firefox、Chrome、Microsoft Edge、Safari瀏覽器都支援Wasm。詳細資訊可參考官網「https://webassembly.org/」的說明。

在撰寫此篇文章的時間點,Blazor目前尚屬於實驗階段的專案,在正式版發佈之前規格可能異動頻煩,因此尚不建議在正式專案中使用,本文的內容藉時也可能過期。

在這篇文章中,將要介紹如何在Visual Studio 2017開發工具中建立第一個Blazor網站。

安裝軟體

在本文撰寫的時間,Blazor內建在.NET Core 2.1版。需要更新開發工具與軟體版本如下:

l Visual Studio 2017 需更新到15.7以上版本。

l 下載並安裝 .NET Core 2.1 SDK或以上版本,本文範例使用NET Core 2.1.3 RC1版本(https://www.microsoft.com/net/download/dotnet-core/sdk-2.1.300-rc1)。

l 下載 ASP.NET Core Blazor Language Services(https://marketplace.visualstudio.com/items?itemName=aspnet.blazor)。

Blazor原始程式碼亦開放置於Github,可參閱:「https://github.com/aspnet/Blazor」。

 

建立網站

開發環境安裝完成後便可以利用Visual Studio 2017開發工具來建立一個ASP.NET Core Web Applicaion。點選Visual Studio 2017開發工具的「File」-「New」-「Project」項目,在「New Project」對話盒中,選取左方「Installed」清單 - 「Visual C#」程式語言,從「Web」分類中,選取「ASP.NET Core Web Application」,請參考下圖所示,設定專案名稱,如「MyBlazorDemo」,以及專案存放路徑後按下「OK」鍵:

clip_image002

圖 1:建立「ASP.NET Core Web Application」專案。

在「New ASP.NET Core Web Application」對話盒中,確認左上方的清單選取「.NET Core」,右上方的清單ASP.NET Core版本為「ASP.NET Core 2.0」或以上版本,選取下方的「Blazor」樣版專案,然後按下「OK」按鈕建立專案,請參考下圖所示:

clip_image004

圖 2:選取「Blazor」樣版專案。

新建立的範本網站結構如下圖所示,「Pages」資料夾用來存放元件(Component)程式碼;「Shared」資料夾用來存放共用的元件程式碼;「App.cshtml」用來設定路由;「global.json」用來進行組態設定;「Program.cs」檔案中包含一個「Main」方法,定義程式進入點:

clip_image006

圖 3:「Blazor」範本網站檔案結構。

基本上第一個Blazor網站便已經建立完成,「Pages」資料夾下的「Index.cshtml」是網站首頁。在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),Visual Studio開發工具便會自動啟動一個網站裝載程式(Web Host),請參考下圖所示:

clip_image008

圖 4:啟動網站裝載程式。

接著會啟動瀏覽器,可看到首頁如下圖所示:

clip_image010

圖 5:網站首頁執行結果。

「Pages」資料夾下的「Index.cshtml」檔案實作了Blazor元件(Blazor Component),首頁的內容只包含靜態HTML標籤如下列表:

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />


 

「Pages」資料夾下的「Counter.cshtml」與「FetchData.cshtml」元件除了包含HTML標籤之外,還包含了使用C#語言撰寫的程式邏輯。

若在檢視「Index.cshtml」在瀏覽器中執行的結果,可以看到瀏覽器接收到以下標籤與程式碼,透過JavaScript(blazor.js)與「MyBlazorDemo.dll」組件來運行:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width">
    <title>MyBlazorDemo</title>
    <base href="/" />

    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />

    <link href="css/site.css" rel="stylesheet" />

</head>
<body>
    <app>Loading...</app>

    <script src="_framework/blazor.js" main="MyBlazorDemo.dll" entrypoint="MyBlazorDemo.Program::Main" references="Microsoft.AspNetCore.Blazor.Browser.dll,Microsoft.AspNetCore.Blazor.dll,
Microsoft.Extensions.DependencyInjection.Abstractions.dll,
Microsoft.Extensions.DependencyInjection.dll,mscorlib.dll,netstandard.dll,
System.Core.dll,System.dll,System.Net.Http.dll" linker-enabled="true"></script>
</body>
</html>


「MyBlazorDemo.dll」組件預設會放在「專案\bin\Debug\netstandard2.0」資料夾之中。「MyBlazorDemo.Program」類別中的「Main」方法將會是程式的進入點。

 

建立Hello元件

一個Razor(*.cshtml)檔案定義一個Blazor元件(Blazor Component)。一個Blazor元件是一個.NET類別,定義一個可以在網頁中重複使用的Web使用者介面(Web UI)。

讓我們開始來試寫一個「Hello」元件,首先在專案中Pages資料夾加入一個「Razor View」,從「Solution Explorer」視窗 -「Pages」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,請參考下圖所示:

clip_image012

圖 6:建立新項目。

從「Add New Item」對話盒中,選取「Visual C#」-「ASP.NET Core」分類下的,「Razor View」項目,然後在下方將「Name」設定為「Hello.cshtml」最後按下「Add」按鈕,請參考下圖所示:

clip_image014

圖 7:在專案中加入「Razor View」項目。

在「Hello.cshtml」檔案中加入以下程式碼:

@page "/hello"
<h1> Hello </h1>
<p>
  Name :
  <input placeholder="Enter Your Name " bind="@myName" />
</p>
<br />
<p>
  Message : @msg
</p>
<button class="btn btn-primary" onclick="@SayHello"> Click me </button>
@functions {
  string myName;
  string msg;
  void SayHello()
  {
    msg = $"Hello {myName}";
  }
}

「Hello.cshtml」檔案第一行以「@page」指示詞開始。其後的字串定義了路由。也就是說「Hello」元件會負則處理瀏覽器送過來的「/hello」請求。

Hello元件使用標準的HTML標籤定義,程式處理邏輯則是使用Razor語法(使用C#語言)。HTML標籤與程式邏輯將會在編譯階段轉換成一個元件類別(Component Class),「Hello.cshtml」檔案的名稱就被拿來當做類別的名稱(不含附檔名)。以此例而言「Hello」元件的完整類別名稱為「MyBlazorDemo.Pages.Hello」,此類別將會自動繼承自「Microsoft.AspNetCore.Blazor.Components.BlazorComponent」類別。

「@functions」區塊中定義了「Hello」類別的成員與元件的邏輯,其中「myName」與「msg」將編譯成「private」欄位(Field),「SayHello」則變成方法,你也可以在其中撰寫事件處理程式碼。

你可以使用「@屬性名稱」或「@欄位名稱」語法來設定資料繫結,例如<input>欄位之中透過「bind="@myName"」attribute繫結到「myName」欄位:

<input placeholder="Enter Your Name " bind="@myName" />

或者直接使用「屬性名稱」或「欄位名稱」語法來設定資料繫結,例如以下範例程式碼:

<input placeholder="Enter Your Name " bind="myName" />

事件註冊的語法有點類似JavaScript,使用HTML attribute,例如以下範例程式碼註冊按鈕的「Click」事件觸發後,將會叫用「Hello」元件的「SayHello」方法:

<button class="btn btn-primary" onclick="@SayHello"> Click me </button>

 

元件測試

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:

http://localhost:2505/hello

這個範例程式的執行結果參考如下圖所示,網頁中將會包含一個文字方塊,與一個按鈕:

clip_image016

圖 8:「Hello」元件。

只要在文字方塊中輸入名字,再按下按鈕就可以在下方看到歡迎訊息,請參考下圖所示:

clip_image018

圖 9:「Hello」元件執行結果。

使用元件

若有一個「ServerTime」元件程式如下列表:

<p>
  Server Time is : @t
</p>


@functions {
   string t = DateTime.Now.ToLongTimeString();
}


 

「ServerTime」元件不需要路由,而是提供功能讓其它元件來重複叫用,因此不需要在檔案上方加上「@page」指示詞來定義路由。同時,為了讓網站所有元件都可以使用到它,我們將「ServerTime.cshtml」檔案放在網站中「Shared」資料夾下。

接著修改「Hello.cshtml」檔案,加入「< ServerTime>」標籤,便可以使用「ServerTime」元件:

 

@page "/hello"
<h1> Hello </h1>
<p>
  Name :
  <input placeholder="Enter Your Name " bind="myName" />
</p>
<br />
<p>
  Message : @msg
</p>

<p>
  <ServerTime />
</p>
<button class="btn btn-primary" onclick="@SayHello"> Click me </button>
@functions {
string myName;
string msg;
void SayHello()
{
  msg = $"Hello {myName}";
}
}


選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。

在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:

http://localhost:2505/hello

這個範例程式的執行結果參考如下圖所示:

clip_image020

圖 10:重複使用元件。

使用參數

元件可以設計參數,只要將參數定義成屬性,並在屬性前方套用「Parameter」Attribute,例如修改「ServerTime.cshtml」檔案,加入一個「format」屬性,用於設定時間顯示格式:

<p>
  Server Time is : @GetTime()
</p>


@functions {

[Parameter] string format { get; set; }


string GetTime()
{

  return DateTime.Now.ToString(format);
}

}


 

修改「Hello.cshtml」檔案,使用「ServerTime」元件時,利用HTML attribute「format="tt hh:mm:ss"」設定參數:

@page "/hello"
<h1> Hello </h1>
<p>
  Name :
  <input placeholder="Enter Your Name " bind="myName" />
</p>
<br />
<p>
  Message : @msg
</p>

<p>
  <ServerTime format="tt hh:mm:ss" />
</p>
<button class="btn btn-primary" onclick="@SayHello"> Click me </button>
@functions {
string myName;
string msg;
void SayHello()
{
  msg = $"Hello {myName}";
}
}

 

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下URL:

http://localhost:2505/hello

這個範例程式的執行結果參考如下圖所示:

clip_image022

圖 11:使用參數。

設計ASP.NET Core MVC應用程式

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N180719702
出刊日期: 2018/7/25

在這篇文章中,將要介紹如何利用Visual Studio 2017開發工具,在ASP.NET Core MVC網站中使用模型檢視控制器 (Model – View - Controller,MVC)的設計模式來開發ASP.NET應用程式,並透過Entity Framework Core存取SQL Server LocalDB檔案型資料庫。

讓我們從Visual Studio 2017開發環境中新建一個空白的ASP.NET Core MVCWeb Application網站開始。啟動Visual Studio 2017開發環境。從Visual Studio開發工具「File」-「New」-「Project」項目,在「New Project」對話盒中,選取左方「Installed」清單 -「Visual C#」程式語言,從「.NET Core」分類中,選取「ASP.NET Core Web Application」。請參考下圖所示,設定專案名稱以及專案存放路徑,然後按下「OK」鍵。

clip_image002

圖 1:新建一個空白的ASP.NET Core Web Application網站。

在「New ASP.NET Core Web Application」對話盒中,確認左上方的清單選取「.NET Core」,右上方的清單ASP.NET Core版本為「ASP.NET Core 2.0」,選取下方的「Empty」樣版專案,清除勾選下方的「Enable Docker Support」核取方塊,確定右方的「Authentication」項目設定為「No Authentication」,然後按下「OK」按鈕建立專案,請參考下圖所示:

clip_image004

圖 2:選取「Empty」樣版專案。

設定Bower組態

Bower類似Visual Studio中的Nuget工具,可用來管理前端靜態文件,首先在專案中加入「.bowerrc」檔案設定Bower組態。從「Solution Explorer」視窗 –網站根資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,從「Add New Item」對話盒中,選取「ASP.NET Core」分類下的,「Text File」項目,然後在下方將「Name」設定為「.bowerrc」,最後按下「Add」按鈕,請參考下圖所示:

clip_image006

圖 3:將入「.bowerrc」檔案。

在「.bowerrc」檔案中加入以下程式碼,設定Bower會將套件放在網站「wwwroot/lib」資料夾之中:

{
"directory": "wwwroot/lib"
}

加入「bower.json」檔案。注意:Visual Studio 2017 15.5.x版之前,「Add New Item」對話盒,提供「Bower Configuration File」範本,但在Visual Studio 2017 15.7.x版之後,不再提供此範本,因此此步驟直接加入「JSON File」範本檔案。從「Solution Explorer」視窗 ,網站根資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,從「Add New Item」對話盒中,選取「ASP.NET Core」分類下的,「JSON File」項目,然後在下方將「Name」設定為「bower.json」,最後按下「Add」按鈕,請參考下圖所示:

clip_image008

圖 4:加入「JSON File」。

修改「bower.json」檔案中的「dependencies」項目,加入「Bootstrap」套件,只要儲存修改中的「bower.json」檔案,Visual Studio就會自動安裝套件,可以檢視「Output」視窗查看安裝報告:

{
  "name": "asp.net",
  "private": true,
  "dependencies": {
    "bootstrap": "4.1.1"
  }
}

 

上一個步驟也可以利用圖型介面來安裝套件。從「Solution Explorer」- 專案資料夾上方按滑鼠右鍵,從快捷選單選擇「Manage Bower Packages」選項,開啟「Manage Bower Packages」對話盒,請參考下圖所示 ,若沒有看到此選項,請關掉Visual Studio 開發工具,再重開。

clip_image010

圖 5:「Manage Bower Packages」。

在「Manage Bower Packages」對話盒點選「Browse」項目,在上方的文字方塊中,安裝「bootstrap」,請參考下圖所示:

clip_image012

圖 6:使用「Manage Bower Packages」對話盒安裝「bootstrap 」。

Bootstrap套件安裝完成之後,畫面看起來如下圖所示:

clip_image014

圖 7:套件安裝在「wwwroot\lib」資料夾中。

 

建立與使用模型(Model)

從「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「Models」,請參考下圖所示:

clip_image016

圖 8:新增「Models」資料夾。

從「Solution Explorer」視窗 -「Models」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」項目,從「Add New Item」對話盒中,選取「Visual C#」-「ASP.NET Core」-「Code」分類下的,「Class」項目,請參考下圖所示:

clip_image018

圖 9:加入模型類別。

然後在下方將「Name」設定為「Employee」,最後按下「Add」按鈕,請參考下圖所示:

clip_image020

圖 10:加入Employee類別。

在「Employee」類別檔案之中,為類別定義「Id」、「Name」、「Height」與「Married」四個屬性:

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

namespace EFCDemo.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 = "婚姻狀態" )]
    public bool Married { get; set; }
  }
}

 

從「Solution Explorer」視窗,專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,開啟「Add New Item」對話盒,從右上方文字方塊輸入「config」搜尋,選取「App Settings File」,設定名稱為「appsettings.json」,然後按下「Add」按鈕,建立檔案,請參考下圖所示:

clip_image022

圖 11::建立「appsettings.json」檔案。

修改「appsettings.json」檔案,設定連接字串的資料庫伺服器為「(localdb)\\MSSQLLocalDB」;資料庫為「EmployeeDB」,並使用Windows驗證連接到資料庫:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=EmployeeDB;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

 

設計DbContext類別

從「Solution Explorer」視窗 -「Models」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」,選取「Code」分類下的「Class」項目,然後在下方將「Name」設定為「MyDbContext」最後按下「Add」按鈕,然後加入以下程式碼,修改「MyDbContext」類別程式碼,使其繼承自「DbContext」類別,並在類別中定義一個名為「Employees」,型別為「DbSet< Employee >」的屬性,並且使用相依性插入(Depenency Injection)在建構函式中插入服務「DbContextOptions< MyDbContext >」:

 

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace EFCDemo.Models {
  public class MyDbContext : DbContext {
    public MyDbContext( DbContextOptions<MyDbContext> options )
          : base( options ) { }

    public DbSet<Employee> Employees { get; set; }
  }
}

 

在Startup設定與使用服務

修改專案中的「Startup.cs」檔案,在「ConfigureServices」方法加入以下程式,設定資料庫連接字串,將從「appsettings.json」檔案檔案中的「DefaultConnection」而來,並且叫用「IServiceCollection」實體的「AddDbContext」方法,註冊「MyDbContext」物件。接著在「Configure」方法之中,加入使用服務的程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EFCDemo.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EFCDemo {
  public class Startup {
    public IConfiguration Configuration { get; }

    public Startup( IConfiguration config ) {
      Configuration = config;
    }
    public void ConfigureServices( IServiceCollection services ) {
      services.AddMvc( );
      string cnstr = Configuration ["ConnectionStrings:DefaultConnection"];
      services.AddDbContext<MyDbContext>( options =>
           options.UseSqlServer( cnstr ) );
    }

    public void Configure( IApplicationBuilder app , IHostingEnvironment env ) {
      app.UseDeveloperExceptionPage( );
      app.UseStaticFiles( );
      app.UseMvcWithDefaultRoute( );
    }
  }
}

 

從Visual Studio 2017開發工具 -「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Edit XXX.csproj」選項,編輯專案檔案,請參考下圖所示:

clip_image024

圖 12:修改專案檔案。

加上以下「DotNetCliToolReference」的設定:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.8" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.3" />
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
  </ItemGroup>

</Project>


 

建立資料庫

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。開啟「Developer Command Prompt for VS2017」視窗,先利用「cd」指令切換到專案檔案所在的資料夾:

cd 專案檔案所在的資料夾

接著在提示字元中輸入以下指令:

dotnet ef migrations add initial

執行結果,請參考下圖所示:

clip_image026

圖 13:使用Migration。

接著Visual Studio會在專案中建立一個「Migrations」資料夾,裏頭包含多個C#檔案。選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。然後在提示字元中輸入指令,以更新資料庫:

dotnet ef database update

執行結果,請參考下圖所示:

clip_image028

圖 14:更新資料庫。

資料庫預設將會存放在以下資料夾中:

C:\Users\登入帳號

 

設計控制器

接下來我們將利用Visual Studio 2017工具,快速建立新增、刪除、修改、查詢Employee資料表資料的程式碼。從Visual Studio 2017開發工具 -「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「Controllers」。從「Controllers」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Controller」項目,請參考下圖所示:

clip_image030

圖 15:新增控制器。

在「Add Scaffold」對話盒中選取「MVC Controller with views, using Entity Framework」項目,然後按下「Add 」按鈕,請參考下圖所示:

clip_image032

圖 16:選取控制器範本。

在「Add MVC Controller with views, using Entity Framework」對話盒,將「Model class」設定為「Employee」類別,勾選下方的「Generate views」核取方塊,然後設定控制器名稱(Controller name)為「EmployeesController」,然後按下「Add」按鈕,請參考下圖所示:

clip_image034

圖 17:新增「EmployeesController」控制器。

Visual Studio 2017便會在「Views\Employees」資料夾下,新增「Index.cshtml」、「Edit.cshtml」、「Details.cshtml」、「Delete.cshtml」、「Create.cshtml」檢視檔案,以及在「Controllers」資料夾下,新增控制器程式碼「EmployeesController.cs」,「EmployeesController」類別程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using EFCDemo.Models;

namespace EFCDemo.Controllers {
  public class EmployeesController : Controller {
    private readonly MyDbContext _context;

    public EmployeesController( MyDbContext context ) {
      _context = context;
    }

    // GET: Employees
    public async Task<IActionResult> Index( ) {
      return View( await _context.Employees.ToListAsync( ) );
    }

    // GET: Employees/Details/5
    public async Task<IActionResult> Details( int? id ) {
      if ( id == null ) {
        return NotFound( );
      }

      var employee = await _context.Employees
          .SingleOrDefaultAsync( m => m.Id == id );
      if ( employee == null ) {
        return NotFound( );
      }

      return View( employee );
    }

    // GET: Employees/Create
    public IActionResult Create( ) {
      return View( );
    }

    // POST: Employees/Create
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create( [Bind( "Id,Name,Height,BirthDate,Married" )] Employee employee ) {
      if ( ModelState.IsValid ) {
        _context.Add( employee );
        await _context.SaveChangesAsync( );
        return RedirectToAction( nameof( Index ) );
      }
      return View( employee );
    }

    // GET: Employees/Edit/5
    public async Task<IActionResult> Edit( int? id ) {
      if ( id == null ) {
        return NotFound( );
      }

      var employee = await _context.Employees.SingleOrDefaultAsync( m => m.Id == id );
      if ( employee == null ) {
        return NotFound( );
      }
      return View( employee );
    }

    // POST: Employees/Edit/5
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit( int id , [Bind( "Id,Name,Height,BirthDate,Married" )] Employee employee ) {
      if ( id != employee.Id ) {
        return NotFound( );
      }

      if ( ModelState.IsValid ) {
        try {
          _context.Update( employee );
          await _context.SaveChangesAsync( );
        }
        catch ( DbUpdateConcurrencyException ) {
          if ( !EmployeeExists( employee.Id ) ) {
            return NotFound( );
          } else {
            throw;
          }
        }
        return RedirectToAction( nameof( Index ) );
      }
      return View( employee );
    }

    // GET: Employees/Delete/5
    public async Task<IActionResult> Delete( int? id ) {
      if ( id == null ) {
        return NotFound( );
      }

      var employee = await _context.Employees
          .SingleOrDefaultAsync( m => m.Id == id );
      if ( employee == null ) {
        return NotFound( );
      }

      return View( employee );
    }

    // POST: Employees/Delete/5
    [HttpPost, ActionName( "Delete" )]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> DeleteConfirmed( int id ) {
      var employee = await _context.Employees.SingleOrDefaultAsync( m => m.Id == id );
      _context.Employees.Remove( employee );
      await _context.SaveChangesAsync( );
      return RedirectToAction( nameof( Index ) );
    }

    private bool EmployeeExists( int id ) {
      return _context.Employees.Any( e => e.Id == id );
    }
  }
}

 

Index檢視

檢視共用的HTML與程式,一般建議放在版面配置頁(Layout)檔案中,本文為了簡單起見,省略定義版面配置頁這個步驟。因此在每一個檢視檔案中,都需要個別引用Bootstrap樣式。修改Index檢視(Index.cshtml)檔案,在<head>標籤最下方加入以下程式碼:

<link href = "~/lib/bootstrap/dist/css/bootstrap.css" rel = "stylesheet" />

<style>

body {

margin: 15px;

}

</style>

在</body>標籤之上,加入以下程式碼:

<script src = "~/lib/bootstrap/dist/js/bootstrap.js"></script>

目前「Index.cshtml」檔案的內容看起來如下:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model IEnumerable<EFC.Models.Employee>

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Index </title>
    <link href = "~/lib/bootstrap/dist/css/bootstrap.css" rel = "stylesheet" />
   <style>
        body {
            margin: 15px;
        }
    </style>
</head>
<body>
    <p>
        <a asp-action = "Create"> Create New </a>
    </p>
    <table class = "table">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor( model  = > model.Name )
                </th>
                <th>
                    @Html.DisplayNameFor( model => model.Height )
                </th>
                <th>
                    @Html.DisplayNameFor( model => model.Married )
                </th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @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.Married )
                    </td>
                    <td>
                        <a asp-action = "Edit" asp-route-id = "@item.Id">Edit</a> |
                        <a asp-action = "Details" asp-route-id = "@item.Id">Details</a> |
                        <a asp-action = "Delete" asp-route-id = "@item.Id">Delete</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <script src = "~/lib/bootstrap/dist/js/bootstrap.js"> </script>
</body>
</html>

 

Details檢視

和上一個步驟一樣,在Details檢視(Details.cshtml)檔案中,引用Bootstrap樣式,目前「Details.cshtml」檔案程式碼看起來如下:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model EFC.Models.Employee

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Details </title>
    <link href = "~/lib/bootstrap/dist/css/bootstrap.css" rel = "stylesheet" />
</head>
<body>

    <div>
        <h4> Employee </h4>
        <hr />
        <dl class = "dl-horizontal">
            <dt>
                @Html.DisplayNameFor( model => model.Name )
            </dt>
            <dd>
                @Html.DisplayFor( model => model.Name )
            </dd>
            <dt>
                @Html.DisplayNameFor( model => model.Height )
            </dt>
            <dd>
                @Html.DisplayFor( model => model.Height )
            </dd>
            <dt>
                @Html.DisplayNameFor( model => model.Married )
            </dt>
            <dd>
                @Html.DisplayFor( model => model.Married )
            </dd>
        </dl>
    </div>
    <div>
        <a asp-action = "Edit" asp-route-id = "@Model.Id"> Edit </a> |
        <a asp-action = "Index"> Back to List </a>
    </div>
    <script src = "~/lib/bootstrap/dist/js/bootstrap.js"> </script>
</body>
</html>

 

Create檢視

和上一個步驟一樣,在Create檢視(Create.cshtml)檔案中,引用Bootstrap樣式,目前「Create.cshtml」檔案程式碼看起來如下:

 

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model EFCDemo.Models.Employee

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Create </title>
    <link href = "~/lib/bootstrap/dist/css/bootstrap.css" rel = "stylesheet" />
</head>
<body>

    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class = "col-md-4">
            <form asp-action = "Create">
                <div asp-validation-summary = "ModelOnly" class = "text-danger"></div>
                <div class = "form-group">
                    <label asp-for = "Name" class = "control-label"></label>
                    <input asp-for = "Name" class = "form-control" />
                    <span asp-validation-for = "Name" class = "text-danger"></span>
                </div>
                <div class = "form-group">
                    <label asp-for = "Height" class = "control-label"></label>
                    <input asp-for = "Height" class = "form-control" />
                    <span asp-validation-for = "Height" class = "text-danger"></span>
                </div>
                <div class = "form-group">
                    <label asp-for = "BirthDate" class = "control-label"></label>
                    <input asp-for = "BirthDate" class = "form-control" />
                    <span asp-validation-for = "BirthDate" class = "text-danger"></span>
                </div>
                <div class = "form-group">
                    <div class = "checkbox">
                        <label>
                            <input asp-for = "Married" /> @Html.DisplayNameFor( model => model.Married )
                        </label>
                    </div>
                </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>
    <script src = "~/lib/bootstrap/dist/js/bootstrap.js"> </script>
</body>
</html>

 

Edit檢視

和上一個步驟一樣,在Edit檢視(Edit.cshtml)檔案中,引用Bootstrap樣式,目前「Edit.cshtml」檔案程式碼看起來如下:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model EFCDemo.Models.Employee

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width=device-width" />
    <title> Edit </title>
    <link href = "~/lib/bootstrap/dist/css/bootstrap.css" rel = "stylesheet" />
</head>
<body>

    <h4> Employee </h4>
    <hr />
    <div class = "row">
        <div class = "col-md-4">
            <form asp-action = "Edit">
                <div asp-validation-summary = "ModelOnly" class = "text-danger"></div>
                <input type = "hidden" asp-for = "Id" />
                <div class = "form-group">
                    <label asp-for = "Name" class = "control-label"></label>
                    <input asp-for = "Name" class = "form-control" />
                    <span asp-validation-for = "Name" class = "text-danger"></span>
                </div>
                <div class = "form-group">
                    <label asp-for = "Height" class = "control-label"></label>
                    <input asp-for = "Height" class = "form-control" />
                    <span asp-validation-for = "Height" class = "text-danger"></span>
                </div>
                <div class = "form-group">
                    <label asp-for = "BirthDate" class = "control-label"></label>
                    <input asp-for = "BirthDate" class = "form-control" />
                    <span asp-validation-for = "BirthDate" class = "text-danger"></span>
                </div>
                <div class = "form-group">
                    <div class = "checkbox">
                        <label>
                            <input asp-for = "Married" /> @Html.DisplayNameFor( model => model.Married )
                        </label>
                    </div>
                </div>
                <div class = "form-group">
                    <input type = "submit" value = "Save" class = "btn btn-default" />
                </div>
            </form>
        </div>
    </div>

    <div>
        <a asp-action = "Index"> Back to List </a>
    </div>
    <script src = "~/lib/bootstrap/dist/js/bootstrap.js"> </script>
</body>
</html>

 

Delete檢視

和上一個步驟一樣,在Delete檢視(Delete.cshtml)檔案中,引用Bootstrap樣式,目前「Delete.cshtml」檔案程式碼看起來如下:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model EFC.Models.Employee

@{
    Layout  =  null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name = "viewport" content = "width = device-width" />
    <title> Delete </title>
    <link href = "~/lib/bootstrap/dist/css/bootstrap.css" rel = "stylesheet" />
</head>
<body>

    <h3> Are you sure you want to delete this? </h3>
    <div>
        <h4> Employee </h4>
        <hr />
        <dl class = "dl-horizontal">
            <dt>
                @Html.DisplayNameFor( model => model.Name )
            </dt>
            <dd>
                @Html.DisplayFor( model => model.Name )
            </dd>
            <dt>
                @Html.DisplayNameFor( model => model.Height )
            </dt>
            <dd>
                @Html.DisplayFor( model => model.Height )
            </dd>
            <dt>
                @Html.DisplayNameFor( model => model.Married )
            </dt>
            <dd>
                @Html.DisplayFor( model => model.Married )
            </dd>
        </dl>

        <form asp-action = "Delete">
            <input type = "hidden" asp-for = "Id" />
            <input type = "submit" value = "Delete" class = "btn btn-default" /> |
            <a asp-action = "Index"> Back to List </a>
        </form>
    </div>
    <script src = "~/lib/bootstrap/dist/js/bootstrap.js"> </script>
</body>
</html>

 

測試與執行

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下網址(URL)

http://localhost:52290/Employees

執行結果參考如下,點選畫面中「Create」超連結,便進入新增資料頁面:

clip_image036

圖 18:Index檢視執行結果。

輸入資料之後,按下「Create」按鈕,資料將新增到資料庫中,並回到清單頁,請參考下圖所示:

clip_image038

圖 19:Create檢視執行結果。

在清單頁可以看到新增的資料,點選後方的「Edit」連結,便可進入資料修改畫面,請參考下圖所示:

clip_image040

圖 20:Index檢視執行結果。

修改完成之後,按「Save」超連結,便可自動儲存資料,請參考下圖所示:

clip_image042

圖 21:Edit檢視執行結果。

在清單頁,點選資料後方的「Delete」連結,便可進入資料刪除確認畫面,請參考下圖所示:

clip_image044

圖 22:Delete檢視執行結果。

按下「Delete」按鈕,資料將從資料庫刪除。

從現有資料庫建立ASP.NET Core MVC應用程式

$
0
0

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

在這篇文章中,將要介紹如何利用Visual Studio 2017開發工具,在ASP.NET Core MVC網站中使用模型檢視控制器 (Model – View - Controller,MVC)的設計模式來開發ASP.NET應用程式,並透過Entity Framework Core資料庫優先設計方式來存取SQL Server Express現有的Northwind資料庫。

Northwind資料庫的下載與安裝說明,請參考微軟網站:

https://docs.microsoft.com/zh-tw/dotnet/framework/data/adonet/sql/linq/downloading-sample-databases

 

建立空白專案

讓我們從Visual Studio 2017開發環境中新建一個空白的ASP.NET Core MVCWeb Application網站開始。啟動Visual Studio 2017開發環境。從Visual Studio開發工具「File」-「New」-「Project」項目,在「New Project」對話盒中,選取左方「Installed」清單 -「Visual C#」程式語言,從「.NET Core」分類中,選取「ASP.NET Core Web Application」。請參考下圖所示,設定專案名稱以及專案存放路徑,然後按下「OK」鍵。

clip_image002

圖 1:新建一個空白的「ASP.NET Core Web Application」網站。

在「New ASP.NET Core Web Application」對話盒中,確認左上方的清單選取「.NET Core」,右上方的清單ASP.NET Core版本為「ASP.NET Core 2.1」,選取下方的「Empty」樣版專案,清除勾選下方的「Enable Docker Support」核取方塊,確定右方的「Authentication」項目設定為「No Authentication」,然後按下「OK」按鈕建立專案,請參考下圖所示:

clip_image004

圖 2:選取「Empty」樣版專案。

建立與使用模型(Model)

根據微軟官方文件(https://docs.microsoft.com/en-us/ef/core/get-started/aspnetcore/existing-db)上的步驟,需要在網站專案中手動安裝幾個套件。不過現在只要使用Visual Studio 2017建立的ASP.NET Core 2.1範本專案,都不再需要手動安裝以下套件:

Microsoft.EntityFrameworkCore.SqlServer

Microsoft.EntityFrameworkCore.Tools

甚至在必要時,Visual Studio也會自動幫你安裝一些程式碼產生工具套件:

Microsoft.VisualStudio.Web.CodeGeneration.Design

下一步是建立實體模型。從Visual Studio 2017開發工具「Tools」- 「Nuget Package Manager」-從選單選擇「Package Manager Console」選項,開啟「Package Manager Console」對話盒,直接執行以下指令指定連接字串、資料提供者(Provider)進行反向工程步驟,就可以從現有資料庫來建立模型:

Scaffold-DbContext "Server=.\sqlexpress;Database=Northwind;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

指令後的「-OutputDir」參數用來指定將產生的類別存放的資料夾,以本例來說為「Models」,若資料夾不存在會自動建立。

clip_image006

圖 3:進行反向工程。

若不想要建立所有資料表的模型,我們可以利用「-Tables」參數來設定想使用的資料表。

指令執行完成後,專案中將會產生一個「NorthwindContext.cs」檔案,用來讓Entity Framework Core存取資料庫,資料庫的每一個資料表,會產生一個對應的模型類別,參考下圖為執行結果:

clip_image008

圖 4:工具自動產生模型類別。

預設「NorthwindContext」類別程式碼,自動繼承自「DbContext」類別,並且已產生程式碼使用相依性插入(Depenency Injection)在建構函式中插入服務「DbContextOptions<NorthwindContext>」,參考以下範例程式碼:

namespace EFCDBFirst.Models {
  public partial class NorthwindContext : DbContext {
    public NorthwindContext( ) {
    }

    public NorthwindContext( DbContextOptions<NorthwindContext> options )
        : base( options ) {
    }
//以下略

 

我們需要註解NorthwindContext類別中的OnConfiguring方法,避免將資料庫連接字串等敏感資訊直接寫死在程式中,後續改用組態檔方式統一設定:

    protected override void OnConfiguring( DbContextOptionsBuilder optionsBuilder ) {
//      if ( !optionsBuilder.IsConfigured ) {
//#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
//        optionsBuilder.UseSqlServer( "Server=.\\sqlexpress;Database=Northwind;Trusted_Connection=True;" );
//      }
    }

稍後我們將要設計存取「Region」資料表的控制器與檢視,目前工具產生的「Region」類別程式碼如下,包含一個「RegionId」與一個「RegionDescription」屬性,「Territories」導覽屬性用來存取關聯的「Territories」物件:

using System;
using System.Collections.Generic;

namespace EFCDBFirst.Models {
  public partial class Region {
    public Region( ) {
      Territories = new HashSet<Territories>( );
    }

    public int RegionId { get; set; }
    public string RegionDescription { get; set; }

    public ICollection<Territories> Territories { get; set; }
  }
}

 

使用appsettings.json組態

加入「appsettings.json」組態設定。從「Solution Explorer」視窗,專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」項目,開啟「Add New Item」對話盒,從右上方文字方塊輸入「config」搜尋,選取「App Settings File」,設定名稱為「appsettings.json」,然後按下「Add」按鈕,建立檔案,請參考下圖所示:

clip_image010

圖 5:建立「appsettings.json」檔案。

修改「appsettings.json」檔案,設定連接字串的資料庫伺服器為「.\\sqlexpress」;資料庫為「Northwind」,並使用Windows驗證連接到資料庫:

{

"ConnectionStrings": {

"DefaultConnection": "Server=.\\sqlexpress;Database=Northwind;Trusted_Connection=True;MultipleActiveResultSets=true"

}

}

在Startup設定與使用服務

修改專案中的「Startup.cs」檔案,在「ConfigureServices」方法加入以下程式,設定資料庫連接字串,將從「appsettings.json」檔案檔案中的「DefaultConnection」而來,並且叫用「IServiceCollection」實體的「AddDbContext」方法,註冊「NorthwindContext」物件。接著在「Configure」方法之中,加入使用靜態檔案、MVC服務的程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EFCDBFirst.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EFCDBFirst {
  public class Startup {
    public IConfiguration Configuration { get; }

    public Startup( IConfiguration configuration ) {
      Configuration = configuration;
    }
    public void ConfigureServices( IServiceCollection services ) {
      services.AddDbContext<NorthwindContext>( options => options.UseSqlServer( Configuration.GetConnectionString( "DefaultConnection" ) ) );

      services.AddMvc( );
    }

    public void Configure( IApplicationBuilder app , IHostingEnvironment env ) {
      if ( env.IsDevelopment( ) ) {
        app.UseDeveloperExceptionPage( );
      }
      app.UseStaticFiles( );
      app.UseMvcWithDefaultRoute( );
    }
  }
}

 

設計控制器

接下來我們將利用Visual Studio 2017工具,快速建立新增、刪除、修改、查詢「Region」資料表資料的程式碼。從Visual Studio 2017開發工具 -「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「Controllers」。從「Controllers」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Controller」項目,請參考下圖所示:

clip_image012

圖 6:新增控制器。

在「Add Scaffold」對話盒中選取「MVC Controller with views, using Entity Framework」項目,然後按下「Add 」按鈕,請參考下圖所示:

clip_image014

圖 7:選取控制器範本。

在「Add MVC Controller with views, using Entity Framework」對話盒,將「Model class」設定為「Region」類別;「Data context class」為「NorthwindContext」,勾選下方的「Generate views」核取方塊,然後設定控制器名稱(Controller name)為「RegionsController」,接著按下「Add」按鈕,請參考下圖所示:

clip_image016

圖 8:新增「RegionsController」控制器。

Visual Studio 2017便會在「Views\Regions」資料夾下,新增「Index.cshtml」、「Edit.cshtml」、「Details.cshtml」、「Delete.cshtml」、「Create.cshtml」檢視檔案,以及在「Controllers」資料夾下,新增控制器程式碼「RegionsController.cs」。

 

使用標籤協助程式

為了在檢視中使用標籤協助程式(Tag Helper),我們統一在「Views」資料夾加入一個「_ViewImports.cshtml」檔案。從Visual Studio 開發工具「Solution Explorer」視窗 -「Views」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」選項,從「Add New Item」對話盒中,選取「Web」分類中的「Razor View Imports」,檔案名稱設定為「_ViewImports.cshtml」,然後按下「Add」按鈕,請參考下圖所示:

clip_image018

圖 9:加入「_ViewImports.cshtml」檔案。

測試與執行

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下網址(URL)

https://localhost:44313/Regions

執行結果參考如下:

clip_image020

圖 10:查詢Region資料表資料。

點選畫面中「Create」超連結,便進入新增資料頁面,輸入資料之後,按下「Create」按鈕,資料將新增到資料庫中,並回到清單頁,請參考下圖所示

clip_image022

圖 11:Create檢視執行結果。

在清單頁可以看到新增的資料,點選後方的「Edit」連結,便可進入資料修改畫面,請參考下圖所示:

clip_image024

圖 12:Index檢視執行結果。

修改完成之後,按「Save」超連結,便可自動儲存資料,請參考下圖所示:

clip_image026

圖 13:Edit檢視執行結果。

在清單頁,點選資料後方的「Delete」連結,便可進入資料刪除確認畫面,請參考下圖所示:

clip_image028

圖 14:Delete檢視執行結果。

按下「Delete」按鈕,資料將從資料庫刪除。

Repository

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N180819802
出刊日期: 2018/8/22

考量到未來資料存取程式碼可能會有變動的需求,你可能會選擇使用Repository Pattern來為應用程式加入開發上的彈性。Repository包含描述資料操作的介面(interface),以及實作此介面的物件,封裝資料層(data layer)的程式碼,包含操作資料的邏輯,並將它們對應到一個實體模型(Entity Model)。在這篇文章中,將簡介如何在ASP.NET Core MVC專案之中,加入Repository Pattern,設計資料查詢的網頁。

 

建立ASP.NET Core MVC應用程式

讓我們從Visual Studio 2017開發環境中新建一個ASP.NET Core MVCWeb Application範本網站開始。啟動Visual Studio 2017開發環境。從Visual Studio開發工具「File」-「New」-「Project」項目,在「New Project」對話盒中,選取左方「Installed」清單 -「Visual C#」程式語言,從「.NET Core」分類中,選取「ASP.NET Core Web Application」。請參考下圖所示,設定專案名稱以及專案存放路徑,然後按下「OK」鍵。

clip_image002

圖 1:新建一個ASP.NET Core Web Application網站。

在「New ASP.NET Core Web Application」對話盒中,確認左上方的清單選取「.NET Core」,右上方的清單ASP.NET Core版本為「ASP.NET Core 2.1」,選取下方的「Web Application (Model-View-Controller)」樣版專案,清除勾選下方的「Enable Docker Support」核取方塊,確定右方的「Authentication」項目設定為「No Authentication」,然後按下「OK」按鈕建立專案,請參考下圖所示:

clip_image004

圖 2:選取「Web Application (Model-View-Controller)」樣版專案。

建立與使用模型(Model)

在本文中,將建立Employee物件,描述員工ID、Name、Age屬性。模型一般建議放在「Models」資料夾。從「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「Models」。

從「Solution Explorer」視窗 -「Models」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」項目,從「Add New Item」對話盒中,選取「Visual C#」-「ASP.NET Core」-「Code」分類下的,「Class」項目,將「Name」設定為「Employee」,最後按下「Add」按鈕,然後加入以下程式碼:

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

namespace ReposDemo.Models {
  public class Employee {
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
  }
}


設計DbContext類別

在「Models」資料夾中,加入一個「MyDbContext」類別,使其繼承自「DbContext」類別,並在類別中定義一個名為「Employees」,型別為「DbSet< Employee >」的屬性,並且使用相依性插入(Depenency Injection)在建構函式中插入服務「DbContextOptions< MyDbContext >」:

 

using Microsoft.EntityFrameworkCore;

namespace ReposDemo.Models {
  public class MyDbContext : DbContext {
    public MyDbContext( DbContextOptions<MyDbContext> option )
            : base( option ) { }

    public DbSet<Employee> Employees { get; set; }
  }
}

 

定義Respository介面

Repository Pattern中需要定義一個介面定義資料存取操作。在「Models」資料夾中,加入一個「IEmployeeRepository」介面,加入以下程式碼,定義一個「GetAll」方法,用來取回員工清單:

 

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

namespace ReposDemo.Models {
  public interface IEmployeeRepository {
    IEnumerable<Employee> GetAll( );
  }
}

實作Respository介面

下一步讓我們建立類別來實作Respository介面,「Models」資料夾中,加入一個「EmployeeRepository」類別,使其實作「IEmployeeRepository」介面,並加入以下程式碼:

 

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

namespace ReposDemo.Models {
  public class EmployeeRepository : IEmployeeRepository {
    private MyDbContext context;
    public EmployeeRepository( MyDbContext ctx ) {
      context = ctx;
    }
    public IEnumerable<Employee> GetAll( )  {
      return context.Employees;
    }
  }
}

 

「EmployeeRepository」類別將透過建構函式注入(Constructor Injection)取得「MyDbContext」物件,這樣在控制器的程式碼中只需要依賴「IEmployeeRepository」介面,而不用管DbContext由何處何來。

設計控制器

EmployeeController

從Visual Studio 2017開發工具 -「Solution Explorer」視窗 - 專案名稱上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Folder」選項,將新建立的資料夾命名為「Controllers」。

從「Controllers」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Controller」項目。在「Add Scaffold」對話盒中選取「MVC Controller Empty」項目,然後按下「Add 」按鈕。

在「Add Empty MVC Controller」對話盒,將控制器名稱(Controller name)設定為「EmployeeController」,然後按下「Add」按鈕,請參考下圖所示:

clip_image006

圖 3:新增「EmployeeController」控制器。

修改「EmployeeController」控制器程式碼如下,「Index」方法將透過建構函式注入,從Repository介面來取得Employee物件集合:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ReposDemo.Models;

namespace ReposDemo.Controllers {
  public class EmployeeController : Controller {
    private IEmployeeRepository repo;
    public EmployeeController( IEmployeeRepository r ) {
      repo = r;
    }
    public IActionResult Index( ) {
      return View( repo.GetAll( ) );
    }
  }
}


 

設定連接字串

修改「appsettings.json」檔案,設定連接字串的資料庫伺服器為「(localdb)\\MSSQLLocalDB」;資料庫為「EmployeeDB」,並使用Windows驗證連接到資料庫:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=EmployeeDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

 

設計檢視

建立「Index」檢視。將游標停留在控制器程式設計畫面「Index」方法之中,按滑鼠右鍵,從快捷選單選取「Add View」:

clip_image008

圖 4:建立Index檢視。

在「Add View」對話盒中,設定:

1. View name:「Index」。

2. Template:「List」。

3. Model class:「Employee」。

4. 勾選「Reference script libiaries」與「Use a layout page」項目。

然後按下「Add」按鈕。Visual Studio 2017便會在「Views\Home」資料夾下,新增一個「Index.cshtml」檔案,請參考下圖所示:

clip_image010

圖 5:建立Index檢視。

修改「Index.cshtml」檔案的內容如下:

@model IEnumerable<ReposDemo.Models.Employee>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<p>
    總筆數:@Model.Count( )
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor( model => model.ID )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Name )
            </th>
            <th>
                @Html.DisplayNameFor( model => model.Age )
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach ( var item in Model ) {
            <tr>
                <td>
                    @Html.DisplayFor( modelItem => item.ID )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Name )
                </td>
                <td>
                    @Html.DisplayFor( modelItem => item.Age )
                </td>
                <td>
                </td>
            </tr>
        }
    </tbody>
</table>

 

 

在Startup設定與使用服務

修改專案中的「Startup.cs」檔案,在「ConfigureServices」方法加入以下程式,設定資料庫連接字串,將從「appsettings.json」檔案檔案中的「DefaultConnection」而來,並且叫用「IServiceCollection」實體的「AddDbContext」方法,註冊「MyDbContext」物件。接著再叫用「AddTransient」方法,註冊Repository服務:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ReposDemo.Models;

namespace ReposDemo {
  public class Startup {
    public Startup( IConfiguration configuration ) {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices( IServiceCollection services ) {
      services.Configure<CookiePolicyOptions>( options => {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
      } );
      services.AddMvc( ).SetCompatibilityVersion( CompatibilityVersion.Version_2_1 );
      string cnstr = Configuration["ConnectionStrings:DefaultConnection"];
      services.AddDbContext<MyDbContext>( options => options.UseSqlServer( cnstr ) );
      services.AddTransient<IEmployeeRepository , EmployeeRepository>( );
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure( IApplicationBuilder app , IHostingEnvironment env ) {
      if ( env.IsDevelopment( ) ) {
        app.UseDeveloperExceptionPage( );
      } else {
        app.UseExceptionHandler( "/Home/Error" );
        app.UseHsts( );
      }

      app.UseHttpsRedirection( );
      app.UseStaticFiles( );
      app.UseCookiePolicy( );

      app.UseMvc( routes => {
        routes.MapRoute(
            name: "default" ,
            template: "{controller=Employee}/{action=Index}/{id?}" );
      } );
    }
  }
}

 

建立資料庫

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。開啟「Developer Command Prompt for VS2017」視窗,先利用「cd」指令切換到專案檔案所在的資料夾:

cd 專案檔案所在的資料夾

接著在提示字元中輸入以下指令:

dotnet ef migrations add initial

執行結果,請參考下圖所示:

clip_image012

圖 6:使用Migration。

接著Visual Studio會在專案中建立一個「Migrations」資料夾,裏頭包含多個C#檔案。選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。然後在提示字元中輸入指令,以更新資料庫:

dotnet ef database update

執行結果,請參考下圖所示:

clip_image014

圖 7:更新資料庫。

測試與執行

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按F5執行網站首頁(請注意:埠號可能會依據實際上的操作而有所不同,請修改為實際的埠號),然後在瀏覽器輸入以下網址(URL)

http://localhost:44398/Employees

執行結果參考如下:

clip_image016

圖 8:資料查詢。

IEnumerable或IQueryable介面

使用Visual Studio 除錯模式執行網站(F5)時,預設會將Entity Framework Core產生的SQL命令輸出在Visual Studio 「Output」視窗。目前Repository 「GetAll」方法回傳的是「IEnumerable」介面:

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

namespace ReposDemo.Models {
  public interface IEmployeeRepository {
    IEnumerable<Employee> GetAll( );
  }
}

 

若修改控制器程式碼,在叫用「GetAll」方法後,利用「Take」方法,加上篩選條件,將前兩筆資料回傳:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ReposDemo.Models;

namespace ReposDemo.Controllers {
  public class EmployeeController : Controller {
    private IEmployeeRepository repo;
    public EmployeeController( IEmployeeRepository r ) {
      repo = r;
    }
    public IActionResult Index( ) {
      return View( repo.GetAll( ).Take(2) );
    }
  }
}

 

再次按「F5」以除錯模式執行程式碼,你將會發現,雖然只要求取回兩筆資料,但Entity Framework Core產生出來的SQL查詢子句,還是會將所有資料取回來,也就是說篩選資料的動作是在你的程式中做,而不是在資料庫端做掉,你還是會將所有資料表中的資料取回來:

clip_image018

圖 9:檢視Entity Framework Core產生出來的SQL查詢子句。

對於小型資料表,取回所有資料可能問題不大,但對於大型資料表,這將造成效能上的問題。要修訂這個問題,可以改用「IQueryable」介面。修改Repository介面程式碼,使「GetAll」方法回傳「IQueryable」介面:

 

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

namespace ReposDemo.Models {
  public interface IEmployeeRepository {
    IQueryable<Employee> GetAll( );
  }
}

修改Repository類別,使「GetAll」方法回傳「IQueryable」介面:

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

namespace ReposDemo.Models {
  public class EmployeeRepository : IEmployeeRepository {
    private MyDbContext context;
    public EmployeeRepository( MyDbContext ctx ) {
      context = ctx;
    }
    public IQueryable<Employee> GetAll( )  {
      return context.Employees;
    }
  }
}

 

再次按「F5」以除錯模式執行程式碼,你將會發現,Entity Framework Core產生出來的SQL查詢子句,將會包含篩選資料的語法,也就是說篩選資料的動作是在資料庫端做掉,而不是在你的程式中處理:

clip_image020

圖 10:檢視Entity Framework Core產生出來的SQL查詢子句。

如果認真檢視「Output」視窗,你將會發現SQL命令執行兩次:

clip_image022

圖 11:檢視Entity Framework Core產生出來的SQL查詢子句。

這是因為「IQueryable」介面每回需要資料時,都會下達查詢,要求資料,因此在檢視中的這一行程式碼:

<p>

總筆數:@Model.Count( )

</p>

與這一行程式碼,都分別下達一次SQL命令:

@foreach ( var item in Model ) {

}

若要避免這個問題,你可以改回原來回傳「IEnumberable」介面的設計方式。或者,在控制器明確叫用「ToList」或「ToArray」方法,明確地將「IQueryable」介面轉型成「IEnumberable」,例如以下程式碼,在「ToList」這行程式碼執行時,便會馬上執行SQL查詢一次:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ReposDemo.Models;

namespace ReposDemo.Controllers {
  public class EmployeeController : Controller {
    private IEmployeeRepository repo;
    public EmployeeController( IEmployeeRepository r ) {
      repo = r;
    }
  
    public IActionResult Index( ) {
      return View( repo.GetAll( ).Take( 2 ).ToList() );
    }
  }
}

 

如此檢視中的程式碼,便可針對記憶體中的集合物件來進行筆數計算與列舉動作,不需要再下SQL命令從資料庫取回資料。


C# 8新功能概覽 - 1

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N191221401
出刊日期: 2019/12/11

隨著.NET Core 3與Visual Studio 2019開發工具的問市,C# 程式語言也演進到8.0版啦,C# 8 新增了許多有趣的功能,每次改版都希望讓程式碼能夠變短、再變短。在這篇文章中,讓我們來看看一些新增的語法。

唯讀結構成員(Readonly Struct Member)

結構(Struct)的成員可以宣告為唯讀(readonly),也就是說在定義時可以套用「readonly」關鍵字,表示它的成員是不可以被修改的。

參考以下範例程式碼,「Retangle」結構改寫「ToString」方法,顯示出「Retangle」的「Width」、「Height」屬性,以及「Area」屬性的值,「ToString」方法套用「readonly」關鍵字代表它是一個唯讀方法,不會變更其它屬性值:

using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      Retangle r = new Retangle( 10 , 20 );
      Console.WriteLine( r ); // Retangle Width : 10 Height : 20 Area : 200
    }
  }
  struct Retangle {
    public Retangle( int w , int h ) {
      Width = w;
      Height = h;
    }
    public int Width { get; set; }
    public int Height { get; set; }

    public readonly int Area {
      get {
        return Width * Height;
      }
    }
    public readonly override string ToString() {
      return $" Retangle Width : {Width} Height : {Height} Area : { Area } ";
    }
  }

}

 

特別注意,「Area」屬性也要套用「readonly」關鍵字,否則編譯時會出現以下的警告訊息:

Call to non-readonly member 'Retangle.Area.get' from a 'readonly' member results in an implicit copy of 'this'.

這是因為「ToString」方法使用到了「Area」屬性的關係,由於「Area」屬性並不會修改「Area」屬性值,你可以為其套用「readonly」關鍵字。至於「Width」與「Height」兩個自動實作屬性(Auto-implemented properties)則不必特別加上「readonly」關鍵字,編譯器會自動將自動實作屬性的「getter」視為唯讀屬性。

 

預設介面實作(default interface implementation)

以往設計介面(Interface)時,我們總是遵循一套鐵律,介面一旦定義完成,就不要修改,以免破壞既有實作者的程式碼。介面只能包含定義的部分,不能實作。但現在隨著C# 8.0版問市之後,這些規則都將改變了。

在C# 8.0版後,宣告介面成員時,可以加上預設的實作。如此可讓介面因為新功能而改版時更為簡單,並在介面預設實作不符所需時,允許進行改寫。參考以下範例程式碼,展示一個簡單的介面範例,「ISpeak」介面包含一個「Speak」方法;「Person」類別則實作了這個方法:

using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      Person p = new Person();
      p.Speak();
    }
  }
  interface ISpeak {
    void Speak();
  }
  class Person : ISpeak {
    public void Speak() {
      Console.WriteLine( "English" );
    }
  }

}

 

爾後若程式改版,想要在「ISpeak」介面新增一個方法,例如修改程式碼如下,「Speak」方法有一個多載的版本,可以傳入一個參數,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      Person p = new Person();
      p.Speak();
    }
  }
  interface ISpeak {
    void Speak();
    void Speak( string lang );
  }
  class Person : ISpeak {
    public void Speak() {
      Console.WriteLine( "English" );
    }
  }

}

 

這個程式碼編譯時將會失敗,這是因為現有的「Person」類別未實作介面中所有的成員,如此介面加入新功能時,將會破壞既有實作者「Person」類別的程式碼,請參考下圖所示:

clip_image002

圖 1:介面新增將影響既有實作者。

現在我們可以在新增介面成員時,加上預設的實作,這樣就不會影響到既有實作者的程式碼,而既有的「Person」物件,若要叫用介面預設方法,需要轉型成介面,例如修改程式碼如下,這樣程式碼就可以順理的編譯、執行了,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      Person p = new Person();
      p.Speak(); // English

      ( (ISpeak) p ).Speak(); // English
      ( (ISpeak) p ).Speak( "Some Language" ); // Some Language

      ISpeak p2 = new Person();

      p2.Speak(); // English
      p2.Speak( "Some Language" ); // Some Language
    }
  }
  interface ISpeak {
    void Speak();
    void Speak( string lang ) => Console.WriteLine( lang );
  }
  class Person : ISpeak {
    public void Speak() {
      Console.WriteLine( "English" );
    }
  }
}

 

 

現在C# 8介面中可以定義靜態成員,包含:靜態欄位(static field)、靜態方法(static method)。同時介面成員可以設定任意成員存取修飾詞(Access Modifier),例如:「public」、「private」、「internal」、「protected」等等。

因為預設介面實作提供的功能較為跼限,你可以透過參數(Parameter)來增加設計上的彈性,例如我們的「ISpeak」介面「Speak」方法將文字列印到主控台時,想要前置一個「*」字元,我們可以改寫程式如下,利用靜態欄位設定預設的前置字元為「*」號,你也可以叫用「SetPrefix」靜態方法來設定前置字元為「@」號,參考以下範例程式碼:

using System;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      Person p = new Person();
      ( (ISpeak) p ).Speak( "Some Language" ); // *Some Language
      ISpeak.SetPrefix( "@" );
      ISpeak p2 = new Person();
      p2.Speak( "Some Language" ); // @Some Language
    }
  }
  interface ISpeak {
    private static string prefix = "*";
    public static void SetPrefix( string p ) {
      prefix = p;
    }
    void Speak();
    void Speak( string lang ) => Console.WriteLine( prefix + lang );
  }

  class Person : ISpeak {
    public void Speak() {
      Console.WriteLine( "English" );
    }
  }
}

 

using 宣告(Using declaration)

using 宣告(using declaration)是一個變數宣告的語法,前置「using」關鍵字,用來告知編譯器,在執行超出變數有效範圍時,自動釋放相關的資源。舉例來說,若有一個存取檔案的程式如下,在檔案不存在時,利用「File」類別的「Create」方法建立此檔案,並接著馬上將檔案刪除,參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      if ( !File.Exists( "test.txt" ) ) {
        FileStream f = File.Create( "test.txt" );
      }
      File.Delete( "test.txt" );
    }
  }
}

 

 

這個範例執行時將會發生例外錯誤,因為建立檔案後,未釋放檔案相關資源,所以想要刪除檔案時,就會得到例外錯誤,請參考下圖所示:

clip_image004

圖 2:未釋放檔案相關資源例外錯誤。

在C#之前的版本,我們可以將檔案建立語法包在「using」區塊之中,來解決這個問題,請參考以下範例程式碼,語法稍為囉嗦一些,但可以解決IOExeption問題,當程式執行超出「using」區塊,會自動叫用「IDisposable」介面的「Dispose」方法來釋放相關的檔案資源:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      if ( !File.Exists( "test.txt" ) ) {
        using ( FileStream f = File.Create( "test.txt" ) ) {
        };
      }
      File.Delete( "test.txt" );
    }
  }
}

 

而在C# 8版,我們可以使用using 宣告(using declaration)讓語法更為簡潔一些,以達到相同的效果,請參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      if ( !File.Exists( "test.txt" ) ) {
        using FileStream f = File.Create( "test.txt" );
      }
      File.Delete( "test.txt" );
    }
  }

}


 

靜態區域函式(Static Local Function)

參考以下範例程式碼,在C# 7版中,可以在方法(Main)中直接宣告「SayHi」方法,這就叫區域函式(Local Function),但區域函式只可以是實體方法,不可以是靜態方法:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      string SayHi( string s ) {
        return $"Hi, {s} ";
      }
      Console.WriteLine( SayHi( "Mary" ) );
    }
  }
}

 

在C# 8版中新增了靜態區域函式(Static Local Function),區域函式前方可以套用「static」關鍵字,參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
     static string SayHi( string s ) {
        return $"Hi, {s} ";
      }
      Console.WriteLine( SayHi( "Mary" ) );
    }
  }
}

 

在C# 7中,區域函式(Local Function)可以直接存取它的外部函式宣告的變數值,例如以下範例程式碼,「SayHi」方法可以使用到外部「Main」方法宣告的「today」變數:

 

using System;
using System.IO;
namespace ConsoleApp1 {
  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" ) ); // Hi, Mary , today is 11/5/2019
    }
  }
}

 

若將「SayHi」改寫成C# 8 的區域函式(Local Function),參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      string today = DateTime.Now.ToShortDateString();

      static string SayHi( string s  ) {
        return $"Hi, {s} , today is {today}";
      }
      Console.WriteLine( SayHi( "Mary" ) );
    }
  }
}


則在編譯過程中,會產生語法錯誤,顯示靜態區域函式(Static Local Function)無法參考到「today」變數:

clip_image006

圖 3:靜態區域函式(Static Local Function)無法參考到外部函式變數。

要解決這個問題,只要使用參數傳遞的方式,將外部函式宣告的變數值傳入靜態區域函式(Static Local Function)即可,改寫程式碼如下:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      string today = DateTime.Now.ToShortDateString();

      static string SayHi( string s , string today ) {
        return $"Hi, {s} , today is {today}";
      }
      Console.WriteLine( SayHi( "Mary" , today ) ); // Hi, Mary , today is 11/5/2019
    }
  }
}


Disposable ref struct

結構(struct)和類別(class)很像,都可在其中宣告欄位、方法、屬性等成員,結構適用在表示少量資料的情況。預設結構會配置在堆疊(Stack)這塊記憶體中,在某些情況下可能會配置在受管理的堆積(Managed Heap),例如進行裝箱(Boxing、或轉型成介面時。而C# 7.2新增「Ref struct」功能,在宣告結構時,可以套用「ref」關鍵字。

我們先來看一個標準結構範例,參考以下範例程式碼,「ContactInfo」結構包含「Name」、「Title」、「Phone」三個欄位以及一個「Display」方法,我們叫用了「ToString」方法來顯示這些欄位的資訊:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string[] args ) {
      ContactInfo customer;
      customer.Name = "王小明";
      customer.Title = "先生";
      customer.Phone = "(02)1234-5678";
      customer.Display( );
      Console.WriteLine( customer.ToString( ) ); // Name: 王小明 Title: 先生 Phone: (02)1234-5678

    }
  }
  struct ContactInfo {
    public string Name;
    public string Title;
    public string Phone;
    public void Display( ) {
      Console.WriteLine( " Name: " + Name +
             " Title: " + Title +
             " Phone: " + Phone );
    }
  }
}

 

若修改程式將「ContactInfo」指派給「object」型別的變數時,參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string[] args ) {
      ContactInfo customer;
      customer.Name = "王小明";
      customer.Title = "先生";
      customer.Phone = "(02)1234-5678";
      customer.Display( );
      Console.WriteLine( customer.ToString( ) ); // Name: 王小明 Title: 先生 Phone: (02)1234-5678
      object boxcustomer = customer;
    }
  }
  struct ContactInfo {
    public string Name;
    public string Title;
    public string Phone;
    public void Display( ) {
      Console.WriteLine( " Name: " + Name +
             " Title: " + Title +
             " Phone: " + Phone );
    }
  }
}

 

請參考下圖所示,從IL來看,當你將實值型別變數(struct)指派給「object」型別變數,便自動進行「Boxing」動作,將「struct」放到「Managed Heap」之中。

clip_image008

圖 4:自動進行「Boxing」動作。

此外參考以下程式碼,若將結構當做「MyClass」類別的成員,當你建立類別實體時,因為「ContactInfo」結構是類别的成員,同樣會將結構配置於「Managed Heap」之中。

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      ContactInfo customer = new ContactInfo() {
        Name = "王小明" ,
        Title = "先生" ,
        Phone = "(02)1234-5678"
      };
      MyClass c = new MyClass();
      c.ContactInfo = customer;

    }
  }
  struct ContactInfo {
    public string Name;
    public string Title;
    public string Phone;
    public void Display() {
      Console.WriteLine( " Name: " + Name +
             " Title: " + Title +
             " Phone: " + Phone );
    }
  }
  class MyClass {
    public ContactInfo ContactInfo;
  }
}

 

ref 結構(Ref struct)

C# 8 新增「Ref struct」,它的特性包含如下:

  • · 方法參數、區域變數等等的值,只能配置在堆疊(Stack)當中,如此可以減少GC的負擔,不用負責回收記憶體。
  • · 不能宣告成類別或標準結構的靜態(static)或實體(Instance)成員,無法進行裝箱(Boxing)。
  • · 不能夠當做「async」方法或「lambda」運算式的參數
  • · 不能夠動態繫結(Binding)、Boxing、Unboxing或轉換。
  • · 不能實作介面。

若將前文範例中的「ContactInfo」結構定義成「ref 結構」,參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      ContactInfo customer;
      customer.Name = "王小明";
      customer.Title = "先生";
      customer.Phone = "(02)1234-5678";
      customer.Display();
      Console.WriteLine( customer.ToString() ); // Error

    }
  }
  ref struct ContactInfo {
    public string Name;
    public string Title;
    public string Phone;
    public void Display() {
      Console.WriteLine( "Name:" + Name +
             "Title: " + Title +
             "Phone: " + Phone );
    }
  }
}

 

則叫用「ToString」方法則會失敗,這個方法是繼承「System.Object」的「ToString」方法而來,因為「ref struct」不允許Boxing,程式將無法通過編譯,請參考下圖所示:

clip_image010

圖 5:「ref struct」不允許Boxing。

因為不允許Boxing,所以無法將結構指派給「object」型別的變數,以下程式碼一樣無法通過編譯:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      ContactInfo customer = new ContactInfo() {
        Name = "王小明" ,
        Title = "先生" ,
        Phone = "(02)1234-5678"
      };
      object boxcustomer = customer; // error
    }
  }
  ref struct ContactInfo {
    public string Name;
    public string Title;
    public string Phone;
    public void Display() {
      Console.WriteLine( "Name:" + Name +
             "Title: " + Title +
             "Phone: " + Phone );
    }
  }
}

錯誤訊息請參考下圖所示:

clip_image012

圖 6:不允許Boxing。

同樣的,將「ref struct」宣告為「MyClass」類別成員也會發生語法錯誤,無法編譯,參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      ContactInfo customer = new ContactInfo() {
        Name = "王小明" ,
        Title = "先生" ,
        Phone = "(02)1234-5678"
      };
      MyClass c = new MyClass();
      c.ContactInfo = customer;

    }
  }
  ref struct ContactInfo {
    public string Name;
    public string Title;
    public string Phone;
    public void Display() {
      Console.WriteLine( "Name:" + Name +
             "Title: " + Title +
             "Phone: " + Phone );
    }
  }
  class MyClass {
    public ContactInfo ContactInfo; // error
  }
}

錯誤訊息請參考下圖所示:

clip_image014

圖 7:「ref struct」不可宣告為「MyClass」類別成員。

「ref struct」也無法實作介面,例如以下範例程式碼,試圖實作「IDisposable」介面:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string[] args ) {
      ContactInfo customer;
      customer.Name = "王小明";
      customer.Title = "先生";
      customer.Phone = "(02)1234-5678";
      customer.Display( );

    }
  }
  ref struct ContactInfo : IDisposable {
    public string Name;
    public string Title;
    public string Phone;
    public void Display( ) {
      Console.WriteLine( "Name:" + Name +
             "Title: " + Title +
             "Phone: " + Phone );
    }
  }
}

 

程式將無法進行編譯,請參考下圖所示:

clip_image016

圖 8:「ref struct」無法實作介面。

Disposable ref struct

那們我們現在回到主題,C# 8新增的「Disposable ref structs」功能,就是因為原來的「ref struct」無法新增介面,所以便無法實作「IDisposable」介面來釋放相關資源,當然也不能夠將結構宣告在using的區塊之中,例如以下程式碼將會有編譯錯誤:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      using ( ContactInfo customer = new ContactInfo() {
        Name = "王小明" ,
        Title = "先生" ,
        Phone = "(02)1234-5678"
      } ){ } ; // error
    }
  }
  ref struct ContactInfo  {
    public string Name;
    public string Title;
    public string Phone;
    public void Display() {
      Console.WriteLine( "Name:" + Name +
             "Title: " + Title +
             "Phone: " + Phone );
    }
  }
}

錯誤訊息請參考下圖所示:

 

clip_image018

圖 9:「ref struct」不能夠宣告在using的區塊之中。

在C# 8 只要在「ref struct」之中,加上一個「public」的「Dispose」方法,上述的問題就迎刃而解,參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      using ( ContactInfo customer = new ContactInfo() {
        Name = "王小明" ,
        Title = "先生" ,
        Phone = "(02)1234-5678"
      } )  { } ;

    }
  }
  ref struct ContactInfo  {
    public string Name;
    public string Title;
    public string Phone;
    public void Display() {
      Console.WriteLine( "Name:" + Name +
             "Title: " + Title +
             "Phone: " + Phone );
    }
    public void Dispose() {
    }
  }
}

 

或搭配C# 8 「using宣告」新語法來服用,參考以下範例程式碼:

using System;
using System.IO;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      using ContactInfo customer = new ContactInfo() {
        Name = "王小明" ,
        Title = "先生" ,
        Phone = "(02)1234-5678"
      };

    }
  }
  ref struct ContactInfo  {
    public string Name;
    public string Title;
    public string Phone;
    public void Display() {
      Console.WriteLine( "Name:" + Name +
             "Title: " + Title +
             "Phone: " + Phone );
    }
    public void Dispose() {
    }
  }
}

C# 8新功能概覽 - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N191221402
出刊日期: 2019/12/25

在這篇文章中,將延續《C# 8新功能概覽 - 1》一文的內容,介紹一些C# 8版新增的語法。

 

可為 Null 的參考型別(Nullable reference types)

在啟用nullable annotation context後,參考型別的變數都被視為不可為null (nonnullable reference type。若參考型別的變數允許設定null,在宣告變數時,型別之後方可以加上「?」號,此種型別變數稱為

可為 Null 的參考型別(Nullable reference types

那麼為什麼要有可為 Null 的參考型別(Nullable reference types)呢? 我們參考一個簡單的範例,以下「string」型別的「nullable1」變數初始化成null,但下一行程式碼叫用「ToString」方法,在程式執行階段會直接產生「System.NullReferenceException」例外錯誤:

using System;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      string nullable1 = null;
      nullable1.ToString(); // System.NullReferenceException
    }
  }
}

因此為了避免例外錯誤,你需要撰寫一些防護措失,避免在變數為「null」時叫用了「ToString」方法,例如以下程式碼:

using System;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      string nullable1 = null;
      if ( nullable1 != null ) {
        nullable1.ToString();
      }

    }
  }
}


或讓程式更精簡一些,使用「?.」運算子,來程式更簡短些,參考以下範例程式碼:

using System;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      string nullable1 = null;
      nullable1?.ToString();
    }
  }
}

不過有時程式設計師可能會忘記使用這些防呆措失來避免例外錯誤的產生,C# 8可為 Null 的參考型別(Nullable reference types)便可以幫助我們解決這個問題,讓編譯器在程式撰寫階段就自動幫忙做檢查。只要在程式中使用「#nullable enable」這行程式啟用nullable annotation context,而參考型別的變數值可能為null時,編譯器就會顯示警告訊息,參考以下範例程式碼:

using System;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
#nullable enable
      string? nullable2 = null;
       nullable2.ToString(); // Warning CS8602  Dereference of a possibly null reference.ConsoleApp1

#nullable disable
      string? nullable3 = null; //CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context
      nullable3.ToString();

      string nullable1 = null;
      nullable1?.ToString();

    }
  }
}


 

「nullable2.ToString()」這行程式編譯時會顯示警告訊息,請參考下圖所示:

clip_image002

圖 1:編譯階段顯示警告訊息。

「#nullable disable」這行程式則停用nullable annotation context,此時若宣告可為 Null 的參考型別,如範例中的「nullable3」,則編譯器會自動顯示警告訊息,請參考下圖所示:

clip_image004

圖 2:「#nullable disable」。

非同步資料流(Asynchronous stream)

C# 8 版可以使用非同步方式來建立以及使用資料流(Stream)。自.NET Standard 2.1版之後,新增三個介面,「IAsyncDisposable」、「IAsyncEnumerable<T>」、「IAsyncEnumerator<T>」。這些介面可視為「IEnumerable<T>」非同步的版本,.NET Core 3.0已經實作了這三個介面,同樣地,「IDisposable」介面也有一個非同步的版本「IAsyncDisposable」,有了這些介面,意味著你可以在「foreach」與「using」關鍵字之前加上「await」關鍵字,如此將可以用來存取非同步資料流(Asynchronous stream)。

一個回傳非同步資料流的方法必需是一個非同步方法,例如以下範例程式碼,「GetUserList」方法每隔100毫秒回傳序列中一個使用者的名稱,「GetUserList」方法宣告時加上「async」關鍵字;方法回傳型別是「IAsyncEnumerable<T>」,例如範例中的「IAsyncEnumerable<string>」,在方法中透過「yield return」關鍵字回傳資料流中的項目,例如範例中回傳的是陣列項目。

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

namespace ConsoleApp1 {
  class Program {
    async static Task Main( string [] args ) {
      await foreach ( var name in GetUserList() ) {
        Console.WriteLine( name );
      }
    }
    //C# 8  Asynchronous streams
    static async IAsyncEnumerable<string> GetUserList() {
      string [] list = { "Mary" , "Candy" , "Lulu" , "Lisa" };
      for ( int i = 0 ; i < list.Length ; i++ ) {
        await Task.Delay( 100 );
        yield return list [i];
      }
    }
  }
}

 

 

在「Main」方法使用「foreach」存取非同步資料流中的項目時,需要在「foreach」前方加上「await」關鍵字,這樣每當你讀取序列中的項目時,就會以非同步方式來處理。

現在「using」關鍵字之前也可以加上「await」關鍵字,參考以下範例程式碼:

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

namespace ConsoleApp2 {
  class Program {
    static async Task Main( string [] args ) {
      await WriteAsync( @"c:\temp\a.txt" );
      await ReadAsync( @"c:\temp\a.txt" );
    }
    //C# 8  Asynchronous streams
    static async Task ReadAsync( string file ) {
      await using ( var fs = new FileStream( file , FileMode.Open ) ) {
         using ( var sr = new StreamReader( fs ) ) {
          string s = await sr.ReadToEndAsync();
          Console.WriteLine( s );
        }
      }
    }
    static async Task WriteAsync( string file ) {
      await using ( var fs = new FileStream( file , FileMode.Open ) ) {
        await using ( var sw = new StreamWriter( fs ) ) {
          await sw.WriteLineAsync( " Hello World! " );
          await sw.WriteLineAsync( " Hello World! " );
        }
      }
    }
  }
}

 

「FileStream」與「StreamWriter」都實作了「IAsyncDisposable」介面,因此在使用「using」宣告時,可以加上「await」關鍵字;而「StreamReader」類別並未實作「IAsyncDisposable」介面,也沒有適當的「DisposeAsync」方法,因此不可以加上「await」關鍵字,開發工具會提醒你這個錯誤,請參考下圖所示:

clip_image006

圖 3:語法錯誤。

索引和範圍(Indices and range)

C# 8 新增兩個運算子:hat(^)與range (..),透過簡易的語法來存取序列,例如陣列(Array)或Span<T>中的項目。例如你想把陣列中5到10的項目取出,range運算子便是一個很好的選擇。

.NET新增兩個型別來支援這兩個運算子:

· System.Index:代表序列中的索引。

· System.Range :代表一個範圍。

索引位置若為 「n」,則「^n」表示「length-n」,「length」為序列項目個數。參考以下範例程式碼,有一個字串陣列包含以下序列項目,使用索引「0」(list[0])可以存取到陣列中第一個項目;陣列的「Length」目前為「4」,而索引「^4」(list[^4]),套用「length-n」(4 - 4 = 0),同樣可以存取到陣列中第一個項目,依此類推,索引「1」(list[1])可以存取到陣列中第二個項目;索引「^3」(list[^3]),套用「length-n」(4 - 3 = 1),同樣可以存取到陣列中第二個項目:

string [] list = { "Mary" , "Candy" , "Lulu" , "Lisa" };

Console.WriteLine( $" {list[0]} - {list[^4]} " ); // Mary - Mary

Console.WriteLine( $" {list[1]} - {list[^3]} " ); // Candy - Candy

Console.WriteLine( $" {list[2]} - {list[^2]} " ); // Lulu - Lulu

Console.WriteLine( $" {list[3]} - {list[^1]} " ); // Lisa – Lisa

若使用「^0」,則表示要存取索引「4」的項目,由於目前陣列最大索引為「3」,以下範例程式碼則會產生例外錯誤:

var outOfRange = list [^0]; // Exception

我們也可以指定開始與結尾的索引,例如取回索引「1」到索引「3」的項目,但不包含索引「3」,則會取回陣列中第二與第三個項目:

var secondAndThird = list [1..3];

Console.WriteLine( string.Join( " , " , secondAndThird )); // Candy , Lulu

也可以搭配hat(^)運算子指定開始與結尾的索引,以下範例程式碼將取回陣列最後兩個項目;

var lastTwo = list [^2..^0];

Console.WriteLine( string.Join( " , " , lastTwo ) ); // Lulu , Lisa

若不指定開始索引,則預設以索引「0」開始,以下範例程式碼將取回陣列索引「0」到索引「3」的項目,但不包含索引「3」:

var three = list [..3];

Console.WriteLine( string.Join( " , " , three ) ); // Mary , Candy , Lulu

而以下範例程式碼將取回陣列中所有的項目:

var all = list [..^0];

Console.WriteLine( string.Join( " , " , all ) ); // Mary , Candy , Lulu , Lisa

若不指定結尾索引,則取回自開始索引之後的所有項目,以下範例程式碼將取回陣列索引「2」之後的所有項目:

var from3 = list [2..];

Console.WriteLine( string.Join( " , " , from3 ) ); // Lulu , Lisa

範圍可以儲存在Range結構型別的變數,例如以下範例程式碼取回索引「1」到索引「4」的項目,但不包含索引「4」:

Range r = 1..4;

var secondToFour = list [r];

Console.WriteLine( string.Join( " , " , secondToFour ) ); // Candy , Lulu , Lisa

以下附上範例完整程式碼:

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

namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      string [] list = { "Mary" , "Candy" , "Lulu" , "Lisa" };
      Console.WriteLine( $" {list[0]} - {list[^4]} " ); // Mary - Mary
      Console.WriteLine( $" {list[1]} - {list[^3]} " ); // Candy - Candy
      Console.WriteLine( $" {list[2]} - {list[^2]} " ); // Lulu - Lulu
      Console.WriteLine( $" {list[3]} - {list[^1]} " ); // Lisa - Lisa
     
      //var outOfRange = list [^0]; // Exception

      var secondAndThird = list [1..3];
      Console.WriteLine( string.Join( " , " , secondAndThird )); // Candy , Lulu

      var lastTwo = list [^2..^0];
      Console.WriteLine( string.Join( " , " , lastTwo ) ); //  Lulu , Lisa

      var three = list [..3];
      Console.WriteLine( string.Join( " , " , three ) ); // Mary , Candy , Lulu

      var all = list [..^0];
      Console.WriteLine( string.Join( " , " , all ) ); // Mary , Candy , Lulu , Lisa

      var from3 = list [2..];
      Console.WriteLine( string.Join( " , " , from3 ) ); // Lulu , Lisa

      Range r = 1..4;
      var secondToFour= list [r];
      Console.WriteLine( string.Join( " , " , secondToFour ) ); // Candy , Lulu , Lisa

    }
  }
}

 

Null 聯合指派運算子(Null-coalescing assignment operator)

C# 8 新增Null 聯合指派運算子(??=,null-coalescing assignment operator )可以在左方變數為「null」的情況下,將右方的運算元(Operand)指派給左方。

參考以下範例程式碼,在沒有Null 聯合指派(Null-coalescing assignment)之前,我們可能需要撰寫程式檢查List<string>集合變數的值是否為「null」,若為「null」,則建立集合物件實體,這樣叫用「Add」方法將字串加入集合時才不會產生例外錯誤:

static void AssignVar1() {
  List<string> list = null;
  if ( list == null ) {
    list = new List<string>();
  }
  list.Add( "Hello" );
  Console.WriteLine( string.Join( " , " , list ) );
}

另一種方式是透過Null 聯合運算子(??,null-coalescing operator),可以得到相同執行結果,參考以下範例程式碼:

static void AssignVar2() {
  List<string> list = null;
  list = list ?? new List<string>();
  list.Add( "Hello" );
  Console.WriteLine( string.Join( " , " , list ) );
}

C# 8 則新增Null 聯合指派運算子(??=,Null-coalescing assignment operator),程式可以改寫如下,可以得到相同執行結果:

static void AssignVar3() {
  List<string> list = null;
  list ??= new List<string>();
  list.Add( "Hello" );
  Console.WriteLine( string.Join( " , " , list ) );
}

 

字串插值

字串插值的語法也有改良,若要插值的字串包含多行文字,在C# 7版需先出現「$」號,再出現「@」號,順序不可換,而C# 8版則兩個符號順序可以對調,兩種寫法都可以,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ConsoleApp1 {
  class Program {
    static void Main( string [] args ) {
      var h = DateTime.Now.Hour;
      var m = DateTime.Now.Minute;
      Console.WriteLine(
          $@"Now is
            {h}:{m}"
          );

      Console.WriteLine(
         @$"Now is
            {h}:{m}"
         );

      //Now is
      //  5:57
    }
  }
}

 

模式比對(Pattern matching)

模式比對(Pattern matching)可以更容易根據類型(Shape)來驗證與檢查物件。C# 7加入Type Pattern、Constant Pattern,而C# 8 則新增了遞迴模式(Recursive pattern包含Switch 運算式(Switch expression)、位置模式(Positional pattern)、屬性模式(Property pattern)、Tuple模式(Tuple pattern)等等。

Switch 運算式(Switch expression)

Switch 運算式(Switch expression)類似傳統的C# 「switch」陳述式(switch statement)語法,可以根據不同的條件,來產生不同的值,但少掉許多C# 「switch」陳述式必要的「case」、「break」關鍵字,以及大括號 { } 的程式碼,程式可以短,再更短。

「_」符號用來匹配任何運算式(Expression),稱做「Discard Pattern」,通常搭配「Switch 運算式」使用,類似「switch陳述式」的「default」區塊程式碼,參考以下範例程式碼,根據使用者輸入的文字來決定咖啡的大小:

using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
  class Class1 {
    static void Main( string [] args ) {
      Console.WriteLine( "請輸入咖啡的大小(L/M/S):" );
      string s = Console.ReadLine();
      string result = string.Empty;
      switch ( s.ToUpper() ) {
        case "L":
          result = "大杯";
          break;
        case "M":
          result = "中杯" ;
          break;
        case "S":
          result = "小杯" ;
          break;
        default:
          result = "Error…" ;
          break;
      }
      Console.WriteLine( result );
    }
  }
}

讓我們使用「switch 運算式」改寫這個範例程式碼:

using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
  class Class1 {
    static void Main( string [] args ) {
      Console.WriteLine( "請輸入咖啡的大小(L/M/S):" );
      string s = Console.ReadLine();
      string result = s.ToUpper() switch {
        "L" => "大杯",
        "M" => "中杯",
        "S" => "小杯",
        _ => "Error…"
      };
      Console.WriteLine( result );
    }
  }
}

 

「Switch 運算式」中變數的位置出現在「switch」關鍵字的左方,好讓你不會和「switch陳述式(Statement)」語法混淆在一起。「switch陳述式」中的「case」與「:」號,就改用「=>」符號取代,而「default」就改用「_」符號取代。「=>」符號右方則是一個運算式(expression)。

位置模式(Positional pattern)

位置模式(Positional pattern)也稱做「Deconstruction Pattern」,搭配有實作「Deconstruct」方法的型別來使用。「Deconstruct」方法的主要用途是將多個物件的屬性值,一次指派給多個變數。位置模式(Positional pattern)可以讓你檢視物件的屬性值,把這些值當做模式(pattern)進行比對。

參考以下範例程式碼,有三個「Rectangle」,物件,我們想要透過「DisplayInfo」找出「Area」屬性值為「200」且「Perimeter」屬性值為「60」的矩型比對結果,並印出訊息,範例中採用「switch運算式」來進行比對:

using System;

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

    Rectangle rect1 = new Rectangle( 10 , 20 );
    (double area1, double perimeter1) = rect1;
    Console.WriteLine( $"Rectangle 1 Area = {area1}" ); // Rectangle 1 Area = 200
    Console.WriteLine( $"Rectangle 1 Perimeter = {perimeter1}" ); // Rectangle 1 Perimeter = 60


    Rectangle rect2 = new Rectangle( 20 , 10 );
    (double area2, double perimeter2) = rect2;
    Console.WriteLine( $"Rectangle 2 Area = {area2}" ); // Rectangle 2 Area = 200
    Console.WriteLine( $"Rectangle 2 Perimeter = {perimeter2}" ); // Rectangle 2 Perimeter = 60


    Rectangle rect3 = new Rectangle( 5 , 40 );
    (double area3, double perimeter3) = rect3;
    Console.WriteLine( $"Rectangle 3 Area = {area3}" ); // Rectangle 3 Area = 200
    Console.WriteLine( $"Rectangle 3 Perimeter = {perimeter3}" ); // Rectangle 3 Perimeter = 90


    Console.WriteLine( $" {DisplayInfo( rect1 )}  Area = {area1}  Perimeter = {perimeter1}" ); // Yes  Area = 200  Perimeter = 60
    Console.WriteLine( $" {DisplayInfo( rect2 )}  Area = {area2}  Perimeter = {perimeter2}" ); // Yes  Area = 200  Perimeter = 60
    Console.WriteLine( $" {DisplayInfo( rect3 )}  Area = {area3}  Perimeter = {perimeter3}" ); // No ( 200 - 90 )   Area = 200  Perimeter = 90

  }
  static string DisplayInfo( Rectangle rect ) => rect switch
  {
    _ when rect.Area == 200 && rect.Perimeter == 60 => "Yes",
    _ => $"No ( { rect.Area } - { rect.Perimeter } ) "
  };

}


public class Rectangle {
  public Rectangle( int len , int width ) {
    this.Area = len * width;
    this.Perimeter = ( len + width ) * 2;
  }
  public void Deconstruct( out double area , out double perimeter ) {
    area = this.Area;
    perimeter = this.Perimeter;
  }
  public double Area { get; set; }
  public double Perimeter { get; set; }
}

若改用使用位置模式(Positional pattern),程式碼看起來像這樣,顯然程式碼更為簡短了:

using System;

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

    Rectangle rect1 = new Rectangle( 10 , 20 );
    (double area1, double perimeter1) = rect1;
    Console.WriteLine( $"Rectangle 1 Area = {area1}" ); // Rectangle 1 Area = 200
    Console.WriteLine( $"Rectangle 1 Perimeter = {perimeter1}" ); // Rectangle 1 Perimeter = 60


    Rectangle rect2 = new Rectangle( 20 , 10 );
    (double area2, double perimeter2) = rect2;
    Console.WriteLine( $"Rectangle 2 Area = {area2}" ); // Rectangle 2 Area = 200
    Console.WriteLine( $"Rectangle 2 Perimeter = {perimeter2}" ); // Rectangle 2 Perimeter = 60


    Rectangle rect3 = new Rectangle( 5 , 40 );
    (double area3, double perimeter3) = rect3;
    Console.WriteLine( $"Rectangle 3 Area = {area3}" ); // Rectangle 3 Area = 200
    Console.WriteLine( $"Rectangle 3 Perimeter = {perimeter3}" ); // Rectangle 3 Perimeter = 90


    Console.WriteLine( $" {DisplayInfo( rect1 )}  Area = {area1}  Perimeter = {perimeter1}" ); // Yes  Area = 200  Perimeter = 60
    Console.WriteLine( $" {DisplayInfo( rect2 )}  Area = {area2}  Perimeter = {perimeter2}" ); // Yes  Area = 200  Perimeter = 60
    Console.WriteLine( $" {DisplayInfo( rect3 )}  Area = {area3}  Perimeter = {perimeter3}" ); // No ( 200 - 90 )   Area = 200  Perimeter = 90

  }

  static string DisplayInfo( Rectangle rect ) => rect switch
  {
    (200, 60 ) => "Yes",
    var (a, p) => $"No ( {a} - {p} )"
  };
}


public class Rectangle {
  public Rectangle( int len , int width ) {
    this.Area = len * width;
    this.Perimeter = ( len + width ) * 2;
  }
  public void Deconstruct( out double area , out double perimeter ) {
    area = this.Area;
    perimeter = this.Perimeter;
  }
  public double Area { get; set; }
  public double Perimeter { get; set; }
}

 

屬性模式(Property pattern)

位置模式(Positional pattern)有一個小問題:解構(Deconstruct)之後,資料的順序為何,以上例來說出現在第一個位置的是「Area」還是「Perimeter」? 要記憶這些順序有點困難。我們可以改用屬性模式(Property pattern)來降低困擾,它又稱做「object pattern」。

我們來看一下以下範例程式碼,「Rectangle」新增一個「Id」屬性,以及一個可傳三個參數的建構函式(Constructor),我們將三個矩型放到陣列之中:

using System;

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

    Rectangle rect1 = new Rectangle( 10 , 20 , "rect1" );
    (double area1, double perimeter1) = rect1;
    Console.WriteLine( $"Rectangle 1 Area = {area1}" ); // Rectangle 1 Area = 200
    Console.WriteLine( $"Rectangle 1 Perimeter = {perimeter1}" ); // Rectangle 1 Perimeter = 60


    Rectangle rect2 = new Rectangle( 20 , 10 );
    (double area2, double perimeter2) = rect2;
    Console.WriteLine( $"Rectangle 2 Area = {area2}" ); // Rectangle 2 Area = 200
    Console.WriteLine( $"Rectangle 2 Perimeter = {perimeter2}" ); // Rectangle 2 Perimeter = 60


    Rectangle rect3 = new Rectangle( 5 , 40 , "rect3" );
    (double area3, double perimeter3) = rect3;
    Console.WriteLine( $"Rectangle 3 Area = {area3}" ); // Rectangle 3 Area = 200
    Console.WriteLine( $"Rectangle 3 Perimeter = {perimeter3}" ); // Rectangle 3 Perimeter = 90

    Rectangle [] list = { rect1 , rect2 , rect3 };

    foreach ( var item in list ) {
      if ( item.Area == 200 && item.Perimeter == 60 ) {
        Console.WriteLine( $" Id : {item.Id}  Area = {area1}  Perimeter = {perimeter1}" );
      }
    }

    Console.WriteLine();
    Console.WriteLine( "id is not null" );
    foreach ( var item in list ) {
      if ( item is { Id: { } } ) {
        Console.WriteLine( $" Id : {item.Id}  Area = {area1}  Perimeter = {perimeter1}" );
      }
    }

    Console.WriteLine();
    Console.WriteLine( "id is not null and area is 200" );

    foreach ( var item in list ) {
      if ( item is { Id: { }, Area: 200 } ) {
        Console.WriteLine( $" Id : {item.Id}  Area = {item.Area}  Perimeter = {perimeter1}" );
      }
    }

    Console.WriteLine();
    Console.WriteLine( "id is not null and area is 200 , assign Perimeter to per" );
    foreach ( var item in list ) {
      if ( item is { Id: { }, Area: 200, Perimeter: double per } ) {
        Console.WriteLine( $" Id : {item.Id}  Area = {item.Area}  Perimeter = {per}" );
      }
    }
  }
}


public class Rectangle {
  public Rectangle( int len , int width ) {
    this.Area = len * width;
    this.Perimeter = ( len + width ) * 2;
  }
  public Rectangle( int len , int width , string id ) {
    this.Area = len * width;
    this.Perimeter = ( len + width ) * 2;
    this.Id = id;
  }
  public void Deconstruct( out double area , out double perimeter ) {
    area = this.Area;
    perimeter = this.Perimeter;
  }
  public string Id { get; set; }
  public double Area { get; set; }
  public double Perimeter { get; set; }
}

 

 

第一段「foreach」程式是沒有使用模式比對的標準寫法,找出「Area」屬性值為「200」且「Perimeter」屬性值為「60」的物件。

第二段「foreach」程式中使用到「item is { Id: { } }」,意思是要找出「Id」屬性值不為「null」(使用{ } pattern)的物件。

第三段「foreach」程式中使用到「item is { Id: { }, Area: 200 }」,意思是要找出「Id」屬性值不為「null」(使用{ } pattern),且「Area」屬性值為「200」的物件。

第四段「foreach」程式中使用到「item is { Id: { }, Area: 200, Perimeter: double per }」,意思是要找出「Id」屬性值不為「null」(使用{ } pattern),且「Area」屬性值為「200」的物件,並且將「Perimeter」屬性值指派給「per」變數。這個範例程式的執行結果參考如下:

Rectangle 1 Area = 200

Rectangle 1 Perimeter = 60

Rectangle 2 Area = 200

Rectangle 2 Perimeter = 60

Rectangle 3 Area = 200

Rectangle 3 Perimeter = 90

Id : rect1 Area = 200 Perimeter = 60

Id : Area = 200 Perimeter = 60

id is not null

Id : rect1 Area = 200 Perimeter = 60

Id : rect3 Area = 200 Perimeter = 60

id is not null and area is 200

Id : rect1 Area = 200 Perimeter = 60

Id : rect3 Area = 200 Perimeter = 60

id is not null and area is 200 , assign Perimeter to per

Id : rect1 Area = 200 Perimeter = 60

Id : rect3 Area = 200 Perimeter = 90

Tuple模式(Tuple pattern)

有時你需要依賴多個判斷條件來執行程式碼,「Tuple pattern」可以搭配「switch expression」來處理這個問題,例如,我們常常要判斷台灣 3+2郵遞區號所在的位置,此時就可以撰寫以下程式碼:

using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApp1 {
  class Class1 {
    static void Main( string [] args ) {
      Console.WriteLine( ShowPostcalCode("111","43")); // 士林區     力行街
      Console.WriteLine( ShowPostcalCode("106","88")); // 大安區 仁愛路
      Console.WriteLine( ShowPostcalCode("104","14")); // 中山區     松江路
    }
    static string ShowPostcalCode( string first3 , string last2 ) => ( first3, last2 ) switch
    {
      ("111", "43" ) => "士林區     力行街",
      ("103", "42" ) => "大同區     民生西路",
      ("106", "88" ) => "大安區 仁愛路",
      ("104", "61" ) => "中山區 中山北路",
      ("104", "14" ) => "中山區     松江路",
      (_, _ ) => "unknow"
    };
  }
}

[Required]與[BindRequired] Attribute

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N200121501
出刊日期: 2020/1/8

在ASP.NET Core MVC的專案中,我們利用「System.ComponentModel.DataAnnotations」命名空間下的Attribute類別設定模型屬性的資料驗證規則,例如「Range」Attribute用來指定資料欄位值的數值範圍;「StringLength」Attribute 用來指定資料欄位中允許的最小和最大字元長度。這些Attribute會應用在伺服端模型繫結(Model Binding)的過程中進行資料驗證,若用戶端傳送到伺服端的資料不滿足指定的條件約束,就會將「ModelState.IsValid」設定為「false」,而在檢視(View)中,便可以利用「asp-validation-for」與「asp-validation-summary」來顯示錯誤訊息。這篇文章要討論的是兩個非常相似的「Required」與「BindRequired」Attribute。

「Required」Attribute指定表單欄位驗證時,欄位必須包含值。 如果屬性值為「null」、空字串(""),或只包含空白字元,則會引發驗證例外狀況。

我們先來看一下下面「Required」Attribute的範例,「Employee」模型的「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個屬性定義的上方都套用了 [Required] Attribute,要求其值不可以為空白,同時利用「ErrorMessage」屬性自訂了錯誤訊息:「Not Empty!」。

Employee.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace StarterM.Models {
  public class Employee {
    [Display( Name = "Employee Id" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeId { get; set; }
    [Display( Name = "Employee Name" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeName { get; set; }
    [Display( Name = "Birth Date" )]
    [DataType( DataType.Date )]
    [Required( ErrorMessage = "Not Empty!" )]
    public DateTime BirthDate { get; set; }
    [Display( Name = "Age" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public int Age { get; set; }
  }
}

 

在「HomeController」控制器中,我們撰寫了兩個「Create」方法,在標識「HttpPost」Attribute的「Create」方法之中,利用「ModelState.IsValid」屬性的值來驗證資料是否有問題,當資料都不為空白的情況下,讀出這些資料,利用「ViewBag」傳送到檢視顯示在網頁上;若驗證有問題,便從「ModelState」讀出所有錯誤,並將發生錯誤的屬性名稱(Key)與錯誤訊息(ErrorMessage)串接成字串,利用「ViewBag」傳送到檢視顯示在網頁上:

HomeController

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using StarterM.Models;
using System.Linq;

namespace StarterM.Controllers {
  public class HomeController : Controller {
    public IActionResult Index() {
      return View();
    }
    public IActionResult Create() {
      return View();
    }
    [HttpPost]
    public IActionResult Create( Employee emp ) {
      if ( ModelState.IsValid ) {
        ViewBag.result = $@"
        Employee id : {emp.EmployeeId}
        Employee Name : {emp.EmployeeName}
        Birth Date : {emp.BirthDate}
        ";
      }
      else {
        string sp = "<br/>";
        string msg = "";
        foreach ( var item in ModelState ) {
          if ( item.Value.ValidationState == ModelValidationState.Invalid ) {
            msg += $" {sp} {item.Key} : {string.Join( null , item.Value.Errors.Select( i => sp + i.ErrorMessage ) )}";
          }
          msg += sp;
        }
        ViewBag.result = msg;
      }
      return View();
    }
  }
}

 

以下的「Create」檢視程式碼則利用<form>標籤來送出表單資料:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                <div>
                    <label asp-for="EmployeeName"></label>
                    <input asp-for="EmployeeName" />
                    <span asp-validation-for="EmployeeName"></span>
                </div>
                <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

請參考下圖所示,從執行結果可以發現,我們未在表單上填入任何資料,就透過表單將「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個表單欄位傳送到伺服端,模型繫結器(Model Binder)將會根據這些屬性套用的Attribute進行資料驗證。套用[Required] Attribute的「EmployeeId」、「EmployeeName」兩個屬性都是字串型別(參考型別),能夠正確的顯示出我們設定的自訂錯誤訊息「Not Empty!」;而「BirthDate」(DateTime型別)與 「Age」(int型別)顯示的卻是內建的錯誤訊息:「The value '' is invalid.」,很顯然[Required] Attribute對於「DateTime」、「int」等實值型別沒有作用:

clip_image002

圖 1:[Required] Attribute驗證結果。

讓我們在請求中只送出「EmployeeId」與「BirthDate」兩個表單欄位,註解掉「Create」檢視程式中「EmployeeName」與「Age」兩個<input>欄位的相關標籤:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                @*<div>
                    <label asp-for="EmployeeName"></label>
                    <input asp-for="EmployeeName" />
                    <span asp-validation-for="EmployeeName"></span>
                </div>*@
                <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>

                @*<div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>*@
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

 

同樣不要在表單欄位上填入任何資料,將「EmployeeId」、「BirthDate」兩個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。從驗證的結果可以發現,這次表單只提供「EmployeeId」、「BirthDate」兩個表單欄位,換句說請求中未包含「Age」與「EmployeeName」,而「Age」屬性的型別為「int」,套用[Required] Attribute並無法檢測出錯誤,底下沒有印出任何關於「Age」欄位的錯誤訊息,這表示「ModelState」物件沒有記錄任何關於「Age」的錯誤資訊。而屬性型別為「string」的「EmployeeName」則可如預期運作。這是因為「int」型別的資料其預設值為「0」,由於要求「Age」不可為空白,不管用戶端有沒有傳「Age」到伺服端,它的預設值都是「0」,[Required] Attribute無法識別出這個問題。「BirthDate」這次顯示的是內建的錯誤訊息,請參考下圖所示:

clip_image004

圖 2:[Required] Attribute並無法檢測非Nullable型別的錯誤。

「DateTime」型別和「int」型別有相同的問題,它的預設值是「1/1/0001 12:00:00 AM」。修改「Create」檢視程式,這次讓我們在請求中只送出「EmployeeId」與「Age」兩個表單欄位:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                @*<div>
                    <label asp-for="employeename"></label>
                    <input asp-for="employeename" />
                    <span asp-validation-for="employeename"></span>
                </div>
               <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>*@

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

不要在表單欄位上填入任何資料,將「EmployeeId」、「Age」兩個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。從驗證的結果可以發現,[Required] Attribute並無法正確顯示出「Age」的自訂錯誤,顯示的是模型繫結內建的錯誤訊息,而「ModelState」物件沒有記錄任何關於「BirthDate」的錯誤資訊,請參考下圖所示:

clip_image006

圖 3:[Required] Attribute並無法檢測非Nullable型別的錯誤。

從測試中可以得到一個結論,在模型屬性型別為「int」、「DateTime」的情況下,[Required] Attribute無法檢測請求(Request)中空白的表單欄位值。除了這兩個型別之外,不可為Null的型別都有相同的問題。

 

使用Nullable型別

這個問題的其中一個解法便是使用Nullable型別,讓「int」、「DateTime」屬性都變成可允許「Null」的型別(Nullable),修改「Employee」模型的程式如下:

Employee.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace StarterM.Models {
  public class Employee {
    [Display( Name = "Employee Id" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeId { get; set; }
    [Display( Name = "Employee Name" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public string EmployeeName { get; set; }
    [Display( Name = "Birth Date" )]
    [DataType( DataType.Date )]
    [Required( ErrorMessage = "Not Empty!" )]
    public DateTime? BirthDate { get; set; }
    [Display( Name = "Age" )]
    [Required( ErrorMessage = "Not Empty!" )]
    public int? Age { get; set; }
  }
}


 

修改「Create」檢視如下,利用<form>標籤來送出「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個表單欄位資料:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                <div>
                    <label asp-for="EmployeeName"></label>
                    <input asp-for="EmployeeName" />
                    <span asp-validation-for="EmployeeName"></span>
                </div>
               <div>
                    <label asp-for="BirthDate"></label>
                    <input asp-for="BirthDate" />
                    <span asp-validation-for="BirthDate"></span>
                </div>

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

 

這次執行的結果如下,[Required] Attribute 生效了:

clip_image008

圖 4:[Required] Attribute可以檢測Nullable型別的錯誤。

雖然修改「Employee」模型的屬性型別為Nullable可以解決驗證資料不可為空白的問題,但這會造成一個問題,若搭配Entity Framework Core Code First 來設計資料存取程式,屬性的型別會影響到資料庫資料表欄位的結構,Nullable型別對應的欄位結構是「允許Null值」,非Nullable型別對應的欄位結構是「不允許Null值」,因此這個解法並不一定會是你想要的。

[BindRequired] Attribute

另一個解法便是使用[BindRequired] Attribute,強制請求(Request)一定要提供屬性值。現在修改「Employee」類別如下,「BirthDate」、「Age」的型別分別改為不可為Null的「DateTime」與「int」型別:

Employee.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace StarterM.Models {
  public class Employee {
    [Display( Name = "Employee Id" )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired]
    public string EmployeeId { get; set; }
    [Display( Name = "Employee Name" )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired]
    public string EmployeeName { get; set; }
    [Display( Name = "Birth Date" )]
    [DataType( DataType.Date )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired ]
    public DateTime BirthDate { get; set; }
    [Display( Name = "Age" )]
    [Required( ErrorMessage = "Not Empty!" )]
    [Microsoft.AspNetCore.Mvc.ModelBinding.BindRequired]
    public int Age { get; set; }
  }
}

 

請參考下圖所示,從執行結果可以發現,我們未在表單上填入任何資料,就透過表單將「EmployeeId」、「EmployeeName」、「BirthDate」、「Age」四個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。

請求中有包含「EmployeeId」、「EmployeeName」兩個字串型別屬性,其值為「Null」,模型繫結器沒有記錄任何錯誤;而「Age」(int型別,值為0)與「BirthDate」(DateTime型別,值是「1/1/0001 12:00:00 AM」)都各包含兩個錯誤「The value '' is invalid.」,以及「The value for XXX parameter or property was not provided」,顯示的是內建模型繫結錯誤訊息。

clip_image010

圖 5:[BindRequired] Attribute驗證錯誤。

修改「Create」檢視如下,註解掉「EmployeeName」、「BirthDate」兩個表單欄位:

Create.cshtml

@model StarterM.Models.Employee
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title> Create </title>
</head>
<body>
    <h4> Employee </h4>
    <hr />
    <div class="row">
        <div class="col-md-4">
            <form asp-action="Create">
                <div asp-validation-summary="ModelOnly"></div>
                <div>
                    <label asp-for="EmployeeId"></label>
                    <input asp-for="EmployeeId" />
                    <span asp-validation-for="EmployeeId"> </span>
                </div>
                @*<div>
                        <label asp-for="EmployeeName"></label>
                        <input asp-for="EmployeeName" />
                        <span asp-validation-for="EmployeeName"></span>
                    </div>
                    <div>
                        <label asp-for="BirthDate"></label>
                        <input asp-for="BirthDate" />
                        <span asp-validation-for="BirthDate"></span>
                    </div>
                *@

                <div>
                    <label asp-for="Age"></label>
                    <input asp-for="Age" />
                    <span asp-validation-for="Age"></span>
                </div>
                <div>
                    <input type="submit" value="Create" />
                </div>
            </form>
        </div>
    </div>
    <hr />
    <p>
        Result :
        @Html.Raw( ViewBag.result )
    </p>
</body>
</html>

 

同樣不要在表單欄位上填入任何資料,將「EmployeeId」、「Age」兩個表單欄位傳送到伺服端,讓模型繫結器(Model Binder)進行資料驗證。

clip_image012

從驗證的結果可以發現:

· 請求中包含「EmployeeId」,其值是空(Null),不視為錯誤。

· 請求中未包含「EmployeeName」,視為錯誤。

· 請求中不包含「BirthDate」,視為錯誤。

· 請求中包含「Age」,但值是空白,視為錯誤。

由以上測試,得到以下小結:

· [BindRequired] Attribute檢測請求(Request)中是否有包含對應到模型屬性的表單欄位,若是參考型別屬性,表單欄位值可以是空(Null)。 若是實值型別屬性,則請求中必需包含對應的表單欄位,且值不可以為空(Null)。

· [Required] Attribute會檢查屬性值是否為空(Null),並不要求請求(Request)中要包含屬性對應的表單欄位值。

最後要注意,根據微軟文件的說明,[BindRequired] Attribute只適用於表單資料,對XML或JSON資料不起作用。

 

[BindRequired] Attribute自訂錯誤訊息

那麼要怎麼替使用[BindRequired] Attribute驗證的屬性自訂錯誤訊息呢? 這需要修改一下「Startup」類別,參可以下程式碼,在「ConfigureServices」方法中,叫用「AddControllersWithViews」方法設定MVC服務時,傳入「MvcOptions」物件當參數,叫用「SetValueMustNotBeNullAccessor」與「SetMissingBindRequiredValueAccessor」方法來指定自訂錯誤訊息:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace StarterM {
  public class Startup {
    public void ConfigureServices( IServiceCollection services ) {
      //services.AddControllersWithViews();
      services.AddControllersWithViews( options => {
        options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor( _ => " 不可為空(Null)!!" );
        options.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor( _ => "屬性的值未提供!!" );
      } );
    }

    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      app.UseStaticFiles();
      app.UseRouting();

      app.UseEndpoints( endpoints => {
        endpoints.MapControllerRoute(
            name: "default" ,
            pattern: "{controller=Home}/{action=Index}/{id?}" );
      } );

    }
  }
}

用戶端同樣只送出「EmployeeId」、「Age」兩個表單欄位,顯示的訊息如下圖所示:

clip_image014

圖 6:自訂驗證錯誤訊息。

gRPC入門

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N200121502
出刊日期: 2020/1/22

gRPC是一個現代化、開放源碼、高效能的RPC框架 (Framework),可在任何平台上執行,用於遠端程序呼叫(Remote Procedure Calls),很適合分散運算,讓行動裝置應用程式、瀏覽器與後端的服務連結在一起。在這一篇文章之中,我們將介紹在ASP.NET Core設計gRPC服務(Service)以及gRPC用戶端(Client)程式,以了解其基本運作。

什麼是gRPC?

gRPC的特色:

  • 服務定義方式很簡單:使用Protocol Buffers格式定義,它是由Google推出的一種語言中立、平台中立的資料結構序列化協定,比起現下流行的JSON、XML資料格式還要精簡、快速、簡單。Protocol Buffer支援多種程式語言,例如C#、C++、Dart、Go、Java、Node、Objective-C、PHP、Python、Ruby...等等。
  • 安裝簡單,只需一行指令,也很容易延展。
  • 支援多種程式語言與平台。
  • 以HTTP/2為基礎,支援雙向串流(Bi-directional streaming)與驗證。

由於這些優點,gRPC很適合應用在輕量的微服務(microservices),或需要使用多種語言開發的系統,以及點對點(Point-to-point)即時串流服務。

建立ASP.NET Core gRPC伺服器專案

.NET Core 3.x版已經內建了gRPC,當你安裝Visual Studio 2019時,也會順便幫你安裝.NET Core,以下說明利用Visual Studio 2019建立ASP.NET Core gRPC伺服器專案的步驟。從Visual Studio開發工具「File」-「New」-「Project」項目,在「Create a New Project」對話盒中,選取「C#」程式語言,選取「gRPC Service」。請參考下圖所示:

clip_image002

圖 1:建立ASP.NET Core gRPC伺服器專案。

在出現的對話盒中,設定專案名稱為「MyGrpcService.」,設定專案存放路徑,然後按下「Create」鍵,請參考下圖所示:

clip_image004

圖 2:設定專案名稱與專案存放路徑。

在下一個畫面,選取「gRPC Service」項目,然後按下「Create」鍵,請參考下圖所示:

clip_image006

圖 3:使用「gRPC Service」範本建立專案。

gRPC使用合約優先(contract-first)原則來進行開發,服務(Service)與訊息(Message)定義在一個附檔名為「proto」的檔案之中。

首先從「Solution Explorer」視窗 -「「專案\protos」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Item」,在「Add New Item」對話盒中,選取「ASP.NET Core」-「General」分類下的「Protocol Buffer File(通訊協定緩衝區檔案)」,命名為「opera.proto」檔案,請參考下圖所示:

clip_image008

圖 4:加入通訊協定緩衝區檔案。

使用「opera.proto」通訊協定緩衝區檔案定義服務的內容如下,詳細的格式說明,可以參考官網文件,網址在:「https://developers.google.com/protocol-buffers/docs/overview」。

opera.proto

syntax = "proto3";
import "google/protobuf/empty.proto";
option csharp_namespace = "MyGrpcService.Operas";
package operas;

service Opera {
  rpc GetOpera (OperaRequest) returns (OperaReply);
  rpc GetOperaList (google.protobuf.Empty) returns (stream OperaReply);
}

message OperaRequest {
  int32 opera_id = 1;
}

message OperaReply {
  int32 opera_id = 1;
  string title = 2;
  int32 year = 3;
  string composer = 4;
}

 

根據Protocol Buffers官網(https://developers.google.com/protocol-buffers/docs/style)建議,通訊協定緩衝區檔案:

  • proto檔案一列儘量在80個字以內。
  • 使用兩個空白縮排。
  • package的名稱應該全部使用英文小寫,且要對應到資料夾名稱。
  • message名稱(message name)應該使用CamelCase命名法,例如 :「OperaRequest」
  • 欄位名(field name)稱應該以底線做區隔,例如 :「opera_id」。
  • Service名稱與RPC方法名稱都使用CamelCase命名法。

「opera.proto」通訊協定緩衝區檔案定義一個「Opera」gRPC服務,並且用來產生Server Stub相關類別,重點如下:

  • 「syntax」指明「Protocol Buffers」版本。
  • 「csharp_namespace」:動態產生的類別所在的命名空間。
  • 「package」指定套件名稱。
  • 「service」定義服務名稱,例如「Opera」。
  • 「rpc」定義一個RPC方法(Method)。
  • 「message」定義訊息格式。
  • 訊息中的每個欄位都指定一個唯一的數字,這些數字用來識別二進位格式訊息中的欄位,數字由1開始。
  • 「GetOperaList」方法接收空白的請求訊息(google.protobuf.Empty),並使用「stream」關鍵字,表示要回傳伺服端串流(Server Streaming),未來用戶端可以送出一個請求,取回訊息串流,其中包含多個項目的資料,不是只包含一個訊息。gRPC能夠確保用戶端接收訊息的順序與伺服端傳送的訊息一致。

在Visual Studio 2019的「Solution Explorer」視窗,選取「opera.proto」通訊協定緩衝區檔案,利用屬性(Properties)視窗,設定「Build Action」為「Protobuf compiler」,以及設定「gRPC Stub Classes」為「Server only」,請參考下圖所示:

clip_image010

圖 5:設定通訊協定緩衝區檔案「gRPC Stub Classes」為「Server only」。

選取Visual Studio 2019開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。專案編譯時,預設會在「專案名稱\obj\Debug\netcoreapp3.1」資料夾下,產生兩個檔案「Opera.cs」與「OperaGrpc.cs」裏頭包含工具動態產生的「Server Stub」類別,用來讀寫訊息(message)。

設計ASP.NET Core gRPC服務

有了「proto」定義檔之後,可以根據產生的「Server Stub」類別來建立gRPC服務。

從「Solution Explorer」視窗 -「「專案\Services」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」,請參考下圖所示:

clip_image012

圖 6:加入服務類別。

在「Add New Item」對話盒中,選取「ASP.NET Core」-「Code」分類下的「Class」,命名為「OperaService.cs」檔案,請參考下圖所示:

clip_image014

圖 7:設定服務檔案名稱。

gRPC服務可以裝載在ASP.NET Core中執行,在「OperaService」類別中加入以下程式碼:

OperaService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MyGrpcService.Operas;

namespace MyGrpcService.Services {
  public class OperaService : Opera.OperaBase {
    static List<OperaReply> list = new List<OperaReply> {
               new OperaReply
               {
                  OperaId = 1,
                   Title = "Cosi Fan Tutte",
                   Year = 1790,
                   Composer = "Wolfgang Amadeus Mozart",
               },
              new OperaReply
              {
                  OperaId = 2,
                  Title = "Rigoletto",
                  Year = 1851,
                  Composer = "Giuseppe Verdi",
              },
              new OperaReply
              {
                  OperaId = 3,
                  Title = "Nixon in China",
                  Year = 1987,
                  Composer = "John Adams"
              },
              new OperaReply
              {
                  OperaId = 4,
                  Title = "Wozzeck",
                  Year = 1922,
                  Composer = "Alban Berg"
              }
          };
    public override Task<OperaReply> GetOpera( OperaRequest request , ServerCallContext context ) {
      var o = list.Find( o => o.OperaId == request.OperaId );
      return Task.FromResult( o );
    }

    public override async Task GetOperaList( Empty request , IServerStreamWriter<OperaReply> responseStream , ServerCallContext context ) {
      foreach ( var o in list ) {
        await responseStream.WriteAsync( o );
      }
    }
  
  }
}

 

「OperaService」繼承「Opera.OperaBase」型別,此型別的定義是根據「proto」檔案的定義,自動產生的。

 

啟用服務

我們還需要在「Startup」類別啟用gRPC服務,修改「Configure」方法的程式碼:

Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyGrpcService.Services;

namespace MyGrpcService {
  public class Startup {
    public void ConfigureServices( IServiceCollection services ) {
      services.AddGrpc();
    }
    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      app.UseRouting();
      app.UseEndpoints( endpoints => {
        //endpoints.MapGrpcService<GreeterService>();
        endpoints.MapGrpcService<OperaService>();
        endpoints.MapGet( "/" , async context => {
          await context.Response.WriteAsync( "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909" );
        } );
      } );
    }
  }
}

叫用「AddGrpc」方法啟用gRPC服務。叫用「MapGrpcService」方法會自動將服務加入路由管線。在Visual Studio開發工具,按CTRL+F5執行,執行結果參考如下:

clip_image016

圖 8:執行服務。

設計gRPC 用戶端

完成gRPC服務的定義與服務程式碼之後,接著我們便可以在方案之中加入並設計gRPC用戶端程式。從「Solution Explorer」視窗 -「Solution...」項目上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「New Project」項目,從「Add a new project」對話盒中,選取「C#」-「Console App (.NET Core)」,請參考下圖所示:

clip_image018

圖 9:加入用戶端專案到方案。

設定專案名稱為「MygRPCClient.」,設定專案存放路徑,然後按下「Create」鍵,請參考下圖所示:

clip_image020

圖 10:設定用戶端專案名稱。

gRPC用戶端專案需要安裝以下套件來協助開發:

  • 「Grpc.Net.Client」:包含.NET Core用戶端。
  • 「Google.Protobuf」:提供C# protobuf訊息API。
  • 「Grpc.Tools」:工具程式。

從Visual Studio 2019開發工具「Solution Explorer」視窗選取用戶端專案「MygRPCClient.」,按滑鼠右鍵,從快捷選單選擇「Manage NuGet Packages」,開啟「NuGet Package Manager」對話盒,請參考下圖所示:

clip_image022

圖 11:安裝套件。

點選「Browse」項目,並在下方的文字方塊中輸入「Grpc.Net.Client」關鍵字搜尋套件,然後按右方「Install」按鈕進行安裝,請參考下圖所示:

clip_image024

圖 12:安裝「Grpc.Net.Client」套件。

重複上個步驟,安裝「Google.Protobuf」套件,請參考下圖所示:

clip_image026

圖 13:安裝「「Google.Protobuf」套件。

重複上個步驟,安裝「Grpc.Tools」套件,請參考下圖所示:

clip_image028

圖 14:安裝「Grpc.Tools」套件。

從「MyGrpcService」專案複製「opera.proto」檔案到「gRPC Client」專案的「Protos」資料夾,然後利用屬性(Properties)視窗將用戶端「opera.proto」通訊協定緩衝區檔案的「gRPC Stub Classes」屬性設定為「Client Only」,請參考下圖所示:

clip_image030

圖 15:設定用戶端「opera.proto」通訊協定緩衝區檔案的「gRPC Stub Classes」屬性為「Client Only」。

選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。專案編譯時,預設會在「專案名稱\obj\Debug\netcoreapp3.1」資料夾下,產生兩個檔案「Opera.cs」與「OperaGrpc.cs」裏頭包含工具動態產生的「Client Stub」類別,用來讀寫訊息(message)。

最後用戶端專案「Main」方法中加入以下程式碼來叫用服務:

Program.cs

 

using Grpc.Net.Client;
using MyGrpcService.Operas;
using System;
using System.Threading.Tasks;

namespace MygRPCClient {
  class Program {
    static async Task Main( string[] args ) {
      var channel = GrpcChannel.ForAddress( "https://localhost:5001" );
      var client = new Opera.OperaClient( channel );
      var reply = await client.GetOperaAsync(
                        new OperaRequest { OperaId = 1 } );

      Console.WriteLine(reply);
      Console.ReadKey();
    }
  }
}

 

在程式中使用「GrpcChannel.ForAddress」 建立一個跟遠端服務的連線,然後建立gRPC用戶端(Opera.OperaClient),叫用「GetOperaAsync」方法取回結果顯示在主控台。

測試

從「Solution Explorer」視窗 -「Solution...」項目上方,按滑鼠右鍵,從快捷選單選擇「Properties」項目,從對話盒中,選取「Multiple startup projects」,利用右方箭頭調整專案啟動順序,先執行服務,再執行用戶端專案,請參考下圖所示:

clip_image032

圖 16:設定專案啟動順序。

在Visual Studio 2019開發工具,按CTRL+F5執行,執行結果參考如下:

clip_image034

圖 17:範例執行結果。

修改「Main」方法程式碼,改叫用「GetOperaList」方法取回所有Opera資料:

Program.cs

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using MyGrpcService.Operas;
using System;
using System.Threading.Tasks;

namespace MygRPCClient {
  class Program {
    static async Task Main( string[] args ) {
      var channel = GrpcChannel.ForAddress( "https://localhost:5001" );
      var client = new Opera.OperaClient( channel );
      var reply = client.GetOperaList( new Empty() );

      while ( await reply.ResponseStream.MoveNext() ) {
        Console.WriteLine( reply.ResponseStream.Current );
      }
      Console.ReadKey();
    }

  }
}

 

在Visual Studio 2019開發工具,按CTRL+F5執行,執行結果參考如下:

clip_image036

圖 18:取回資料串流。

使用System.Text.Json入門 - 1

$
0
0

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

在過去使用ASP.NET MVC與 .NET Core開發的專案之中,經常會使用到「Json.NET程式庫」來處理JSON(JavaScript Object Notation)資料格式的序列化(Serialization)與還原序列化(Deserialization)的動作,以便於在內、外部系統中做資料交換。在.NET Core 3版之後,內建了「System.Text.Json」套件來處理這個問題,讓你可以不必再依賴非官方的「Json.NET」程式庫,而可以根據喜好來選擇這兩種不同的序列化程式庫。

若專案的目標Framework(Target Framework)設定為「.NET Standard」或「.NET Framework 4.6.1」以上版本,則需要在專案之中使用NuGet手動安裝「System.Text.Json」套件,方能夠透過它來進行開發。

至於效能方面,有許多的文章針對「Json.NET」與「System.Text.Json」這兩個套件做比較,就結果而言,大部分時候.NET Core內建的「System.Text.Json」套件的效能是優於「Json.NET」套件,在此篇文章之中,暫不討論效能這個問題,現在你可以開始考慮開始來試試「System.Text.Json」這個套件,了解它的基本功能與用法。

「System.Text.Json」與「System.Text.Json.Serialization」命名空間包含了大部分的類別與API來處理自訂序列化與還原序列化的情境。讓我們先利用一個主控台應用程式來熟悉一下「System.Text.Json」用法。使用Visual Studio 2019開發工具建立專案時,選擇「主控台應用程式 (.NET Core)」,請參考下圖所示:

clip_image002

圖 1:建立「主控台應用程式 (.NET Core)」。

接著設定專案名稱、與程式存放資料夾,按下「建立」按鈕就可以建立專案,請參考下圖所示:

clip_image004

圖 2:設定新專案。

在專案中加入「Employee」類別,假設有一個員工資料如下:

public class Employee {
  public int EmployeeId { get; set; }
  public string EmployeeName { get; set; }
  public DateTime BirthDate { get; set; }
  public bool IsMarried { get; set; }

  public List<string> Interests { get; set; }
  public WorkExperience WorkExperience { get; set; }
}
public class WorkExperience {
  public string CompanyName { get; set; }
  public int Years { get; set; }
}


 

我們可以使用以下程式碼,引用「System.Text.Json」命名空間,將「Employee」物件的屬性值,利用「JsonSerializer」類別的「Serialize」方法序列化成JSON字串,再利用「JsonSerializer」類別的「Deserialize」方法將JSON字串還原回「Employee」物件:

using System;
using System.Collections.Generic;
using System.Text.Json;

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      Console.WriteLine("Serize : ");
      var jsonString = JsonSerializer.Serialize( emp );
      Console.WriteLine( jsonString );

      Console.WriteLine("===============================");
      Console.WriteLine("Deserialize : ");
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join(',',obj.Interests ));
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }
}

 

這個主控台程式執行的結果參考如下:

clip_image006

圖 3:序列化與還原序列化測試結果。

JsonSerializerOptions物件

「JsonSerializerOptions」物件提供許多屬性可以搭配「JsonSerializer」類別來控制序列化的細節。以下介紹一些常用的屬性。

WriteIndented屬性

為了方便閱讀序列化完的結果,我們可以使用「JsonSerializerOptions」物件的「WriteIndented」屬性,適當地將序列化完的結果美化、排版,例如修改上個範例的程式碼如下:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

叫用「JsonSerializer」類別的「Serialize」方法時,傳入「JsonSerializerOptions」物件做參數,

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

clip_image008

圖 4:序列化與還原序列化測試結果。

通常「JsonSerializerOptions」物件的「WriteIndented」屬性設定為「true」只是方便閱讀,一般生產環境中,建議將其設定為「false」,可使序列化完的結果儘可能精簡,這樣有助於提升傳輸的效能。

序列化到檔案

若想要將序列化完成的結果儲存成文字檔案,則可以利用「System.IO」命名空間下的「File」類別提供的「WriteAllText」方法;而讀取文字檔案可以利用「File」類別提供的「ReadAllText」方法,參考以下範例程式碼,直接將序列化完成的結果儲存成文字檔案:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );

      System.IO.File.WriteAllText( "json.txt" , jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var str = System.IO.File.ReadAllText( "json.txt" );
      var obj = JsonSerializer.Deserialize<Employee>( str );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

使用非同步方式進行序列化

「JsonSerializer」類別也提供了「SerializeAsync」與「DeserializeAsync」方法用於非同步的序列化與還原序列化,參考以下使用非同步方式的程式碼:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

namespace JSONApp1 {

  class Program {
    static async Task Main( string[] args ) {

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );

      using ( FileStream fs = File.Create( "json2.txt" ) ) {
        await JsonSerializer.SerializeAsync( fs , emp );
      }

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      using ( FileStream fs = File.OpenRead( "json2.txt" ) ) {
        var obj = await JsonSerializer.DeserializeAsync<Employee>( fs );
        Console.WriteLine( obj.EmployeeId );
        Console.WriteLine( obj.EmployeeName );
        Console.WriteLine( obj.BirthDate );
        Console.WriteLine( obj.IsMarried );
        Console.WriteLine( string.Join( ',' , obj.Interests ) );
        Console.WriteLine( obj.WorkExperience.CompanyName );
        Console.WriteLine( obj.WorkExperience.Years );
      }
    }
  }
  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

特別注意,「await」關鍵字只能夠在非同步方法之中使用,因此「Main」方法要加上「async」關鍵字宣告為非同步方法,並使其方法的回傳值為「Task」物件。

PropertyNamingPolicy屬性

在撰寫JavaScript時,習慣性名稱會使用camelCase命名法,而C#程式語言則習慣使用PascalCase命名法,此時我們便可以利用「JsonSerializerOptions」物件的「PropertyNamingPolicy」屬性來控制序列化、與還原序列化的屬性名稱要改用camelCase命名法,參考下範例程式碼,為了方便閱讀,直接將序列化完的結果輸出到主控台:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );


      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( jsonString , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

 

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

這個範例程式的執行結果參考如下:

clip_image010

圖 5:序列化與還原序列化測試結果。

特別要注意,叫用「JsonSerializer」類別「Deserialize」方法進行還原序列化時,需使用相同的「JsonSerializerOptions」設定,否則可能會得到例外錯誤。

IgnoreNullValues 屬性

「JsonSerializerOptions」物件的「IgnoreNullValues」 屬性預設值為「 false」,當要序列化的物件屬性值為「null」時,依然會將屬性名稱與「null」包含在序列化完的結果,例如以下的程式碼,故意將「Employee」物件的「EmployeeName」屬性設定為「null」:

 

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        //EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        IgnoreNullValues = false ,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );


      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( jsonString , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }


  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

當「IgnoreNullValues」屬性的值為「false」的情況下,範例程式的執行結果參考如下,包含「EmployeeName」項目:

clip_image012

圖 6:序列化與還原序列化測試結果。

當「IgnoreNullValues」屬性的值為「true」的情況下:

var options = new JsonSerializerOptions {
        IgnoreNullValues = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase ,
        WriteIndented = true
      };

 

範例程式的執行結果參考如下,不包含「EmployeeName」項目:

clip_image014

圖 7:序列化與還原序列化測試結果。

IgnoreReadOnlyProperties 屬性

預設類別的公開(public)屬性都將被序列化。若屬性是唯讀的,或是包含public getter / private setter,只要設定「JsonSerializerOptions」類別的「IgnoreReadOnlyProperties」屬性為「true」,就可以排除這些屬性的序列化,例如以下範例程式碼,「Employee」類別包含唯讀的「CompanyName」屬性,利用「IgnoreReadOnlyProperties」忽略掉唯讀屬性不序列化:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        IgnoreReadOnlyProperties = true ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );


      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( jsonString , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( obj.CompanyName );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }


  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }

    public string CompanyName { get; private set; } = "UCOM";
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

這個範例執行結果請參考下圖,「IgnoreReadOnlyProperties」屬性只會影響序列化的過程,不影響還原序列化,還原序列化的過程中會將之忽略:

clip_image016

圖 8:序列化與還原序列化測試結果。

Encoder屬性

預設非ASCII字元都會以「\uxxx」格式呈現,例如上例程式碼,只要輸入中文字元:

Employee emp = new Employee() {
  EmployeeId = 1 ,
  EmployeeName = "瑪麗" ,
  BirthDate = new DateTime( 2000 , 1 , 2 ) ,
  IsMarried = false ,
  Interests = new List<string> { "游泳" , "爬山" , "跑步" } ,
  WorkExperience = new WorkExperience() {
    CompanyName = "UUU" ,
    Years = 3
  }
};

序列化完的結果看起來如下:

Serize :

{

"EmployeeId": 1,

"EmployeeName": "\u746A\u9E97",

"BirthDate": "2000-01-02T00:00:00",

"IsMarried": false,

"Interests": [

"\u6E38\u6CF3",

"\u722C\u5C71",

"\u8DD1\u6B65"

],

"WorkExperience": {

"CompanyName": "UUU",

"Years": 3

}

}

這個範例執行結果請參考下圖:

clip_image018

圖 9:序列化與還原序列化測試結果。

設定「Encoder」屬性,可以處理多個語言的編碼問題,參考以下範例程式碼,設定「Encoder」屬性的值為「UnicodeRanges.All」,可序列化所有語言,不使用「\uxxx」替代:

 

using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "瑪麗" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "游泳" , "爬山" , "跑步" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };


      var options = new JsonSerializerOptions {
        Encoder = JavaScriptEncoder.Create( UnicodeRanges.All ),
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

這個範例執行結果請參考下圖:

clip_image020

圖 10:序列化與還原序列化測試結果。

UnsafeRelaxedJsonEscaping屬性

要序列化所有字元還可以使用「JavaScriptEncoder」類別的「UnsafeRelaxedJsonEscaping」屬性,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "瑪麗" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "游泳" , "爬山" , "跑步" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };


      var options = new JsonSerializerOptions {
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping ,
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var obj = JsonSerializer.Deserialize<Employee>( jsonString );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

設定「UnsafeRelaxedJsonEscaping」屬性與預設編碼器相較之下,編碼較為寬鬆,例如在網頁程式常使用到的<、>符號都不會進行編碼。

使用System.Text.Json入門 - 2

$
0
0

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

本篇文章延續《使用System.Text.Json入門 - 1》一文的內容,介紹如何在.NET Core 3以上的專案之中,使用「System.Text.Json」套件中提供的類別進行序列化與還原序列化的動作。

「JsonSerializerOptions」物件提供許多屬性可以搭配「JsonSerializer」類別來控制序列化的細節,以下繼續介紹一些此類別常用的屬性。

尾端逗號

預設JSON格式的資料不允許在尾端出現逗號,例如底下格式的資料是有問題的:

{
  "EmployeeId": 1,
  "EmployeeName": "Mary",
  "BirthDate": "2000-01-02T00:00:00",
  "IsMarried": false,
  "Interests": [
    "Swimming",
    "Hiking",
    "Running"
  ],
  "WorkExperience": {
    "CompanyName": "UUU",
    "Years": 3
  },
}

試圖去還原序列化這個JSON文件,便會遭遇到例外錯誤,錯誤訊息如下:

Unhandled exception. System.Text.Json.JsonException: The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options.

這個範例執行結果請參考下圖:

clip_image002

圖 1:序列化與還原序列化測試結果。

若要允許尾端逗號,可以將「JsonSerializerOptions」物件的「AllowTrailingCommas」屬性設定為「true」,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {
      var options = new JsonSerializerOptions {
        AllowTrailingCommas = true
      };
      var str = System.IO.File.ReadAllText( "json.txt" );
      Console.WriteLine(str);
      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( str ,options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

這個範例執行結果請參考下圖:

clip_image004

圖 2:序列化與還原序列化測試結果。

使用ReadCommentHandling處理註解

JSON資料預設不可包含註解,若JSON資料包含以下註解:

{
  "EmployeeId": 1, //serial no
  "EmployeeName": "Mary", //user name
  "BirthDate": "2000-01-02T00:00:00",
  "IsMarried": false,
  "Interests": [
    "Swimming",
    "Hiking",
    "Running"
  ],
  "WorkExperience": {
    "CompanyName": "UUU",
    "Years": 3
  }
}

則試圖去還原序列化這個JSON文件,便會遭遇到例外錯誤,錯誤訊息如下:

Unhandled exception. System.Text.Json.JsonException: '/' is an invalid start of a property name. Expected a '"'

這個範例執行結果請參考下圖:

clip_image006

圖 3:序列化與還原序列化測試結果。

只要指定「JsonSerializerOptions」物件的「ReadCommentHandling」屬性值為「JsonCommentHandling.Skip」就可以解決這個問題,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {
      var options = new JsonSerializerOptions {
        ReadCommentHandling = JsonCommentHandling.Skip ,
        WriteIndented = true
      };
      var str = System.IO.File.ReadAllText( "json.txt" );
      Console.WriteLine( str );
      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( str , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

這個範例執行結果請參考下圖:

clip_image008

圖 4:序列化與還原序列化測試結果。

使用Attribute控制序列化

「System.Text.Json.Serialization」命名空間下包含多個Attribute類別可以控制序列化個別屬性的細節。若有些屬性包含機密的資料,你可以使用內建的[JsonIgnore] attribute來避免序列化。有時為了和不同系統做交換,序列化完的格式和C# 類別的屬性名稱通常不相符,我們可以利用內建的「JsonPropertyName」attribute類別來控制之。

例如以下範例程式碼,利用「System.Text.Json.Serialization」命名空間下的「JsonPropertyName」類別,指定「EmployeeId」屬性名稱序列化完的結果要改為「_id」,「EmployeeName」屬性名稱序列化完的結果要改為「_name」;且利用「JsonIgnore」Attribute設定「WorkExperience」屬性將忽略,不序列化:

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "Mary" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "Swimming" , "Hiking" , "Running" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var jsonString = JsonSerializer.Serialize( emp , options );
      Console.WriteLine( jsonString );


      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( jsonString , options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience?.CompanyName );
      Console.WriteLine( obj.WorkExperience?.Years );

    }
  }

  public class Employee {
    [JsonPropertyName( "_id" )]
    public int EmployeeId { get; set; }
    [JsonPropertyName( "_name" )]
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    [JsonIgnore]
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }

}

這個範例程式的執行結果參考如下:

clip_image010

圖 5:序列化與還原序列化測試結果。

JsonExtensionData attribute

若遇到JSON資料的屬性多於欲還原的物件屬性,可以在類別中定義一個標註「JsonExtensionData」attribute的「Dictionary<string , object>」型別屬性,還原序列化的過程中,會將不相符於物件屬性的JSON資料放在此型別之中,以一個Key配一個Value的形式存在於「Dictionary<string , object>」型別物件中。例如JSON資料如下,包含「BirthDate」與「IsMarried」屬性:

{
  "EmployeeId": 1,
  "EmployeeName": "Mary",
  "BirthDate": "2000-01-02T00:00:00",
  "IsMarried": false,
  "Interests": [
    "Swimming",
    "Hiking",
    "Running"
  ],
  "WorkExperience": {
    "CompanyName": "UUU",
    "Years": 3
  }
}

而以下範例的「Employee」類別不包含「BirthDate」與「IsMarried」屬性,多了一個標註「JsonExtensionData」attribute的「ExtensionData」屬性,

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {
      var options = new JsonSerializerOptions {
        WriteIndented = true
      };
      var str = System.IO.File.ReadAllText( "json.txt" );
      Console.WriteLine(str);
      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );

      var obj = JsonSerializer.Deserialize<Employee>( str ,options );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
   
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

      Console.WriteLine("ExtensionData : ");

      foreach ( var (key, value) in obj.ExtensionData ) {
        Console.WriteLine( $"{key} : {value}" );
      }
    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
 
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
    [JsonExtensionData]
    public Dictionary<string , object> ExtensionData { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

還原序列化之後,可以利用一個「foreach」迴圈,將「ExtensionData」屬性中的項目一一讀取出來。這個範例執行結果請參考下圖:

clip_image012

圖 6:序列化與還原序列化測試結果。

使用JsonDocument進行隨機存取

「JsonDocument」類別可以將JSON字串讀入,直接在記憶體中轉換成一個document object model (DOM)物件,以便隨機存取它的屬性值。

例如先前使用「JsonSerializer」類別將物件序列化成json.txt檔案,檔案內容如下:

{
  "EmployeeId": 1,
  "EmployeeName": "Mary",
  "BirthDate": "2000-01-02T00:00:00",
  "IsMarried": false,
  "Interests": [
    "Swimming",
    "Hiking",
    "Running"
  ],
  "WorkExperience": {
    "CompanyName": "UUU",
    "Years": 3
  }
}

 

我們改用「JsonDocument」類別來讀取它的內容,使用程式如下:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {
  class Program {
    static void Main( string[] args ) {
      var json = System.IO.File.ReadAllText( "json.txt" );
      Console.WriteLine(json);
      Console.WriteLine( "Deserialize : " );
      var doc = JsonDocument.Parse( json );
      Console.WriteLine( doc.RootElement );
      Console.WriteLine( "===============================" );
      var employeeId = doc.RootElement.GetProperty( "EmployeeId" ).GetInt32();
      Console.WriteLine( " EmployeeId : " + employeeId );
      var employeeName = doc.RootElement.GetProperty( "EmployeeName" ).GetString();
      Console.WriteLine( " EmployeeName : " + employeeName );
      var birthDate = doc.RootElement.GetProperty( "BirthDate" ).GetDateTime();
      Console.WriteLine( " BirthDate : " + birthDate );
      var isMarried = doc.RootElement.GetProperty( "IsMarried" ).GetBoolean();
      Console.WriteLine( " IsMarried : " + isMarried );
      var interests = doc.RootElement.GetProperty( "Interests" );
      Console.WriteLine( " Interests : " + interests );
      for ( int i = 0 ; i < interests.GetArrayLength() ; i++ ) {
        Console.WriteLine( interests[i].GetString() );
      }
      var companyName = doc.RootElement.GetProperty( "WorkExperience" ).GetProperty( "CompanyName" );
      Console.WriteLine( " CompanyName : " + companyName );

      var years = doc.RootElement.GetProperty( "WorkExperience" ).GetProperty( "Years" );
      Console.WriteLine( " Years : " + years );
      Console.WriteLine();
    }
  }
}

 

JsonDocument」類別的「Parse」方法可以剖析JSON字串,將它們存放在「RootElement」之中,接著透過「JsonElement」類別的「GetProperty」方法讀取屬性值,「JsonElement」類別提供多個Get開頭的方法,可以將屬性值讀取之後,自動轉型成適當型別。這個範例的執行結果,請參考下圖所示:

clip_image014

圖 7:序列化與還原序列化測試結果。

JsonDocument整合JsonSerializer還原序列化

「JsonDocument」物件的內容若要轉換成物件,可以叫用「JsonElement」類別的「GetRawText」方法取得JSON字串,再搭配「JsonSerializer」類別的「Deserialize」處理之,參考以下範例程式碼:

 

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {
      var json = System.IO.File.ReadAllText( "json.txt" );
      Console.WriteLine( json );
      Console.WriteLine( "Deserialize : " );
      var doc = JsonDocument.Parse( json );
      Console.WriteLine( doc.RootElement );

      var obj = JsonSerializer.Deserialize<Employee>( doc.RootElement.GetRawText() );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );
    }
  }
  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

我們也可以還原序列化個別的屬性,例如上述範例中的「Interests」字串陣列,或「WorkExperience」物件,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

  class Program {
    static void Main( string[] args ) {
      var json = System.IO.File.ReadAllText( "json.txt" );
      Console.WriteLine( json );
      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var doc = JsonDocument.Parse( json );
      Console.WriteLine( doc.RootElement );
      var interests = JsonSerializer.Deserialize<string[]>( doc.RootElement.GetProperty( "Interests" ).GetRawText() );
      Console.WriteLine( string.Join( ',' , interests ) );
      var workExperience = JsonSerializer.Deserialize<WorkExperience>( doc.RootElement.GetProperty( "WorkExperience" ).GetRawText() );
      Console.WriteLine( workExperience.CompanyName );
      Console.WriteLine( workExperience.Years );
    }
  }
  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

 

這個範例執行結果請參考下圖:

clip_image016

圖 8:序列化與還原序列化測試結果。

序列化UTF-8格式

「JsonSerializer」類別提供了「SerializeToUtf8Bytes」方法可將資料序列化成UTF-8位元組陣列 (byte[]),根據官網文件的說明,其效能優於序列化成UTF-16格式的字串約5-10%,參考範例程式碼如下:

 

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace JSONApp1 {

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

      Employee emp = new Employee() {
        EmployeeId = 1 ,
        EmployeeName = "瑪麗" ,
        BirthDate = new DateTime( 2000 , 1 , 2 ) ,
        IsMarried = false ,
        Interests = new List<string> { "游泳" , "爬山" , "跑步" } ,
        WorkExperience = new WorkExperience() {
          CompanyName = "UUU" ,
          Years = 3
        }
      };

      var options = new JsonSerializerOptions {
        WriteIndented = true
      };

      Console.WriteLine( "Serize : " );
      var bytes = JsonSerializer.SerializeToUtf8Bytes( emp , options );
      Console.WriteLine( bytes );

      Console.WriteLine( "===============================" );
      Console.WriteLine( "Deserialize : " );
      var obj = JsonSerializer.Deserialize<Employee>( bytes );
      Console.WriteLine( obj.EmployeeId );
      Console.WriteLine( obj.EmployeeName );
      Console.WriteLine( obj.BirthDate );
      Console.WriteLine( obj.IsMarried );
      Console.WriteLine( string.Join( ',' , obj.Interests ) );
      Console.WriteLine( obj.WorkExperience.CompanyName );
      Console.WriteLine( obj.WorkExperience.Years );

    }
  }

  public class Employee {
    public int EmployeeId { get; set; }
    public string EmployeeName { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsMarried { get; set; }
    public List<string> Interests { get; set; }
    public WorkExperience WorkExperience { get; set; }
  }
  public class WorkExperience {
    public string CompanyName { get; set; }
    public int Years { get; set; }
  }
}

這個範例執行結果請參考下圖:

clip_image018

圖 9:序列化與還原序列化測試結果。

Entity Framework Core Power Tools - 1

$
0
0

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

Entity Framework Core提供了兩套工具程式讓我們對資料庫進行操作,像是進行逆向工程(Reverse engineering),這兩套工具分別為:套件管理員主控台 (Package Manager Console) 命令(使用 NuGet Package Manager下載)與EF Core 命令列工具 (command-line interface (CLI))。習慣使用微軟開發工具的程式設計師,常常會問一個問題:「這些操作是否有圖型介面可以使用 ?」。「Entity Framework Core Power Tools」是你的最佳朋友。在這篇文章中,我們將介紹這個套件,除了提供視覺化的介面來進行逆向工程(Reverse engineering)之外,還提供了哪些好用的功能。

Entity Framework Core Power Tools安裝

首先你需要從Visual Studio 2019開發工具「延伸模組」-「管理延伸模組」選項開啟「管理擴充功能」對話盒,選取左方清單「線上」分類,然後在右上方文字方塊輸入「EF Core Power Tools」關鍵字搜尋,找到後按下「下載」按鈕,從網路下載下來安裝,請參考下圖所示:

clip_image002

圖 1:Entity Framework Core Power Tools安裝。

接著會要求關閉Visual Studio 開發工具,之後便開始進入安裝作業,點選畫面中的「Modify」按鈕,請參考下圖所示:

clip_image004

圖 2:進入安裝作業。

再來會開始安裝動作,直到安裝完成,請參考下圖所示:

clip_image006

圖 3:開始安裝。

從Visual Studio 2019開發工具「檔案」-「新增」-「專案」項目,在「建立新專案」對話盒中,第一個下拉式清單方塊選取「C#」程式語言;從第二個下拉式清單方塊選取「所有平台」;從第三個下拉式清單方塊選取「主控台」,然後選取下方的「主控台應用程式(.NET Core)」範本。請參考下圖所示:

clip_image008

圖 4:建立主控台應用程式。

在「設定新的專案」對話盒中,設定專案名稱與儲存位置,然後按下「建立」按鈕,請參考下圖所示:

clip_image010

圖 5:「設定新的專案」。

逆向工程(Reverse engineering)

若要進行Entity Framework Core逆向工程(Reverse engineering),從現有資料庫的結構描述資訊,來產生開發所需的實體類別程式碼,可以選擇Visual Studio 2019開發工具「方案總管」中的專案名稱,按一下滑鼠右鍵,從快捷選單中,選取「EF Core Power Tools」-「Reverse Engineer」選項,請參考下圖所示:

clip_image012

圖 6:逆向工程(Reverse engineering)。

下一步是連接到資料庫,目前支援多種資料庫,包含SQL Server、SQLite、Postgres、MySQL...等等。由於本範例是以「Entity Framework Core 3.1.x」版,需在「Choose Database Connection」對話盒,勾選「Use EF Core 3.0」核取方塊,然後按一下「Add」按鈕,請參考下圖所示:

clip_image014

圖 7:連接到資料庫。

我們以連接到微軟開發用的SQL Server Express 2019版為例,在「連接屬性」視窗中,設以下屬性,請參考下圖所示:

· 資料來源 (Data Source) :Microsoft SQL Server (SqlClient)。

· 伺服器名稱(Server name)欄位:輸入「.\SQLExpress」。

· 驗證(Authentication):選取「Windows驗證(Windows Authentication)」。

· 選取或輸入資料庫名稱(Select or enter a database name)欄位:選擇「Northwind」資料庫。

clip_image016

圖 8:連接到微軟開發用的SQL Server Express 2019版。

在「Select Tables to Script」對話盒,勾選要使用的資料表(可以選取多個),在此為簡單起見,本例只有選取一個「Region」資料表,然後按下「OK」按鈕,請參考下圖所示:

clip_image018

圖 9:勾選要使用的資料表(可以選取多個)。

參考下圖,在「Generate EF Core Model in Project EFPTDemo」對話盒設定以下內容:

clip_image020

圖 10:「Generate EF Core Model in Project EFPTDemo」對話盒。

按下「OK」鍵,就會根據上個步驟的設定,來產生程式碼。若沒有發生錯誤,完成後,便可以看到執行成功的訊息,請參考下圖所示:

clip_image022

圖 11:執行成功的訊息。

EF Core Power Tools會自動在專案之中,加入「Microsoft.EntityFrameworkCore.SqlServer」套件,並且自動在你指定的「Data」、「Models」資料夾之中產生「NorthwindContext.cs」以及「Region.cs」檔案,請參考下圖所示:

clip_image024

圖 12:自動安裝套件與產生實體類別程式碼。

其中「NorthwindContext.cs」檔案中包含的程式碼,定義一個「NorthwindContext」類別繼承自「DbContext」類別,負責跟實際的資料庫伺服器溝通,「NorthwindContext」類別中定義一個「Regions」屬性,對應到資料庫中「Region」資料表。因為在「Generate EF Core Model in Project EFPTDemo」對話盒之中勾選了「Include connection string in generated code」選項,因此「OnConfiguring」方法中包含程式碼設定了連接到資料庫的連接字串。「OnModelCreating」方法則包含程式碼設定資料表中的欄位資訊:

NorthwindContext.cs檔案程式碼列表

// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
using EFPTDemo.Models;
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace EFPTDemo.Data {
  public partial class NorthwindContext : DbContext {
    public NorthwindContext() {
    }

    public NorthwindContext( DbContextOptions<NorthwindContext> options )
        : base( options ) {
    }

    public virtual DbSet<Region> Regions { get; set; }

    protected override void OnConfiguring( DbContextOptionsBuilder optionsBuilder ) {
      if ( !optionsBuilder.IsConfigured ) {
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.
        optionsBuilder.UseSqlServer( "Data Source=.\\sqlexpress;Initial Catalog=Northwind;Integrated Security=True" );
      }
    }

    protected override void OnModelCreating( ModelBuilder modelBuilder ) {
      modelBuilder.Entity<Region>( entity => {
        entity.HasKey( e => e.RegionId )
            .IsClustered( false );

        entity.Property( e => e.RegionId ).ValueGeneratedNever();

        entity.Property( e => e.RegionDescription ).IsFixedLength();
      } );

      OnModelCreatingPartial( modelBuilder );
    }

    partial void OnModelCreatingPartial( ModelBuilder modelBuilder );
  }
}

 

「Region.cs」檔案則定義了對應到資料表欄位的屬性,請參考以下程式碼列表:

Region.cs檔案程式碼列表

// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EFPTDemo.Models {
  [Table( "Region" )]
  public partial class Region {
    [Key]
    [Column( "RegionID" )]
    public int RegionId { get; set; }
    [Required]
    [StringLength( 50 )]
    public string RegionDescription { get; set; }
  }
}

 

專案中根資料夾下會額外產生一個JSON格式的「efpt.config.json」設定檔案,此檔案記錄了你在EF Power Tools之中所做的設定。

efpt.config.json檔案程式碼列表

{
   "ContextClassName": "NorthwindContext",
   "ContextNamespace": null,
   "DefaultDacpacSchema": null,
   "DoNotCombineNamespace": false,
   "IdReplace": false,
   "IncludeConnectionString": true,
   "ModelNamespace": null,
   "OutputContextPath": "Data",
   "OutputPath": "Models",
   "ProjectRootNamespace": "EFPTDemo",
   "SelectedHandlebarsLanguage": 0,
   "SelectedToBeGenerated": 0,
   "Tables": [
      {
         "HasPrimaryKey": true,
         "Name": "[dbo].[Region]"
      }
   ],
   "UseDatabaseNames": false,
   "UseFluentApiOnly": false,
   "UseHandleBars": false,
   "UseInflector": true,
   "UseLegacyPluralizer": false,
   "UseSpatial": false
}

 

使用DbContext物件

實體類別與DbContext類別產生完之後,就可以利用這些類別來存取資料庫資料,修改「Program」類別程式碼,在「Main」方法中,利用Entity Framework Core查詢「Northwind」資料庫「Region」資料表中的所有資料,參考以下範例程式碼:

using EFPTDemo.Data;
using System;

namespace EFPTDemo {
  class Program {
    static void Main( string[] args ) {
      using ( NorthwindContext context = new NorthwindContext() ) {
        foreach ( var item in context.Regions ) {
          Console.WriteLine($"Region Id : {item.RegionId} , Region Description : {item.RegionDescription}" );
        }
      }
    }
  }
}


這個範例程式的執行結果參考如下:

clip_image026

圖 13:查詢「Northwind」資料庫「Region」資料表中的所有資料。

加入Model Diagram

下一個要介紹的是加入Entity Framework Core Model Diagram的功能。若選擇Visual Studio 2019開發工具「方案總管」中的專案名稱,按一下滑鼠右鍵,從快捷選單中,選取「EF Core Power Tools」-「Add DbContext Model Diagram」選項,請參考下圖所示:

clip_image028

圖 14:加入Model Diagram。

接著會根據專案中的DbContext類別產生出一個副檔名為dbml的檔案,以視覺化的圖型來顯示模型中實體之間的關係與屬性。

clip_image030

圖 15:Model Diagram。

特別注意,Visual Studio 2019需要在安裝時,選擇「Individual components」項目,然後勾選要安裝「Architecture and analysis tools」,才會有視覺化圖型介面來呈現模型。

clip_image032

圖 16:安裝「Architecture and analysis tools」。

Dgml檔案是XML格式,以這個範例而言,產生的「NorthwindContext.dgml」檔案內容如下:

NorthwindContext.dgml檔案程式碼列表

<?xml version="1.0" encoding="utf-8"?>
<DirectedGraph GraphDirection="LeftToRight" xmlns="http://schemas.microsoft.com/vs/2009/dgml">
  <Nodes>
    <Node Id="IModel" Category="Model" Annotations="Relational:MaxIdentifierLength: 128 SqlServer:ValueGenerationStrategy: IdentityColumn" Bounds="-1.4210854715202E-14,-2.8421709430404E-14,197.15,201.92" ChangeTrackingStrategy="ChangeTrackingStrategy.Snapshot" Group="Expanded" Label="NorthwindContext" ProductVersion="3.1.1" PropertyAccessMode="PropertyAccessMode.Default" UseManualLocation="True" />
    <Node Id="Region" Category="EntityType" Annotations="" BaseClass="" Bounds="20,40,157.15,141.92" ChangeTrackingStrategy="ChangeTrackingStrategy.Snapshot" Group="Expanded" IsAbstract="False" Label="Region" Name="Region" />
    <Node Id="Region.RegionDescription" Category="Property Required" AfterSaveBehavior="PropertySaveBehavior.Save" Annotations="MaxLength: 50 Relational:IsFixedLength: True TypeMapping: Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerStringTypeMapping" BeforeSaveBehavior="PropertySaveBehavior.Save" Bounds="40,135.96,117.15,25.96" Field="" IsAlternateKey="False" IsConcurrencyToken="False" IsForeignKey="False" IsIndexed="False" IsPrimaryKey="False" IsRequired="True" IsShadow="False" IsUnicode="True" Label="RegionDescription" MaxLength="50" Name="RegionDescription" PropertyAccessMode="PropertyAccessMode.Default" Type="string" ValueGenerated="None" />
    <Node Id="Region.RegionId" Category="Property Primary" AfterSaveBehavior="PropertySaveBehavior.Save" Annotations="Relational:ColumnName: RegionID TypeMapping: Microsoft.EntityFrameworkCore.Storage.IntTypeMapping" BeforeSaveBehavior="PropertySaveBehavior.Save" Bounds="40,80,67.1566666666667,25.96" Field="" IsAlternateKey="False" IsConcurrencyToken="False" IsForeignKey="False" IsIndexed="False" IsPrimaryKey="True" IsRequired="True" IsShadow="False" IsUnicode="True" Label="RegionId" MaxLength="None" Name="RegionId" PropertyAccessMode="PropertyAccessMode.Default" Type="int" ValueGenerated="None" />
  </Nodes>
  <Links>
    <Link Source="IModel" Target="Region" Category="Contains" />
    <Link Source="Region" Target="Region.RegionDescription" Category="Contains" />
    <Link Source="Region" Target="Region.RegionId" Category="Contains" />
  </Links>
  <Categories>
    <Category Id="Contains" Label="包含" Description="連結的來源是否包含目標物件" CanBeDataDriven="False" CanLinkedNodesBeDataDriven="True" IncomingActionLabel="由下列包含" IsContainment="True" OutgoingActionLabel="包含" />
    <Category Id="EntityType" />
    <Category Id="Model" />
    <Category Id="Property Primary" />
    <Category Id="Property Required" />
  </Categories>
  <Properties>
    <Property Id="AfterSaveBehavior" Group="Property Flags" DataType="System.String" />
    <Property Id="Annotations" Description="Annotations" Group="Model Properties" DataType="System.String" />
    <Property Id="BaseClass" Description="Base class" Group="Model Properties" DataType="System.String" />
    <Property Id="BeforeSaveBehavior" Group="Property Flags" DataType="System.String" />
    <Property Id="Bounds" DataType="System.Windows.Rect" />
    <Property Id="CanBeDataDriven" Label="CanBeDataDriven" Description="CanBeDataDriven" DataType="System.Boolean" />
    <Property Id="CanLinkedNodesBeDataDriven" Label="CanLinkedNodesBeDataDriven" Description="CanLinkedNodesBeDataDriven" DataType="System.Boolean" />
    <Property Id="ChangeTrackingStrategy" Description="Change tracking strategy" Group="Model Properties" DataType="System.String" />
    <Property Id="Expression" DataType="System.String" />
    <Property Id="Field" Description="Backing field" Group="Model Properties" DataType="System.String" />
    <Property Id="GraphDirection" DataType="Microsoft.VisualStudio.Diagrams.Layout.LayoutOrientation" />
    <Property Id="Group" Label="群組" Description="將節點顯示為群組" DataType="Microsoft.VisualStudio.GraphModel.GraphGroupStyle" />
    <Property Id="GroupLabel" DataType="System.String" />
    <Property Id="IncomingActionLabel" Label="IncomingActionLabel" Description="IncomingActionLabel" DataType="System.String" />
    <Property Id="IsAbstract" Label="IsAbstract" Description="IsAbstract" Group="Model Properties" DataType="System.Boolean" />
    <Property Id="IsAlternateKey" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="IsConcurrencyToken" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="IsContainment" DataType="System.Boolean" />
    <Property Id="IsEnabled" DataType="System.Boolean" />
    <Property Id="IsForeignKey" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="IsIndexed" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="IsPrimaryKey" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="IsRequired" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="IsShadow" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="IsUnicode" Group="Property Flags" DataType="System.Boolean" />
    <Property Id="Label" Label="標籤" Description="可註釋物件的可顯示標籤" DataType="System.String" />
    <Property Id="MaxLength" DataType="System.String" />
    <Property Id="Name" Group="Model Properties" DataType="System.String" />
    <Property Id="OutgoingActionLabel" Label="OutgoingActionLabel" Description="OutgoingActionLabel" DataType="System.String" />
    <Property Id="ProductVersion" Label="Product Version" Description="EF Core product version" Group="Model Properties" DataType="System.String" />
    <Property Id="PropertyAccessMode" Group="Property Flags" DataType="System.String" />
    <Property Id="TargetType" DataType="System.Type" />
    <Property Id="Type" Description="CLR data type" Group="Model Properties" DataType="System.String" />
    <Property Id="UseManualLocation" DataType="System.Boolean" />
    <Property Id="Value" DataType="System.String" />
    <Property Id="ValueGenerated" Group="Property Flags" DataType="System.String" />
    <Property Id="ValueLabel" DataType="System.String" />
  </Properties>
  <Styles>
    <Style TargetType="Node" GroupLabel="EntityType" ValueLabel="True">
      <Condition Expression="HasCategory('EntityType')" />
      <Setter Property="Background" Value="#FFC0C0C0" />
    </Style>
    <Style TargetType="Node" GroupLabel="Property Primary" ValueLabel="True">
      <Condition Expression="HasCategory('Property Primary')" />
      <Setter Property="Background" Value="#FF008000" />
    </Style>
    <Style TargetType="Node" GroupLabel="Property Optional" ValueLabel="True">
      <Condition Expression="HasCategory('Property Optional')" />
      <Setter Property="Background" Value="#FF808040" />
    </Style>
    <Style TargetType="Node" GroupLabel="Property Foreign" ValueLabel="True">
      <Condition Expression="HasCategory('Property Foreign')" />
      <Setter Property="Background" Value="#FF8080FF" />
    </Style>
    <Style TargetType="Node" GroupLabel="Property Required" ValueLabel="True">
      <Condition Expression="HasCategory('Property Required')" />
      <Setter Property="Background" Value="#FFC0A000" />
    </Style>
    <Style TargetType="Node" GroupLabel="Navigation Property" ValueLabel="True">
      <Condition Expression="HasCategory('Navigation Property')" />
      <Setter Property="Background" Value="#FF990000" />
    </Style>
    <Style TargetType="Node" GroupLabel="Navigation Collection" ValueLabel="True">
      <Condition Expression="HasCategory('Navigation Collection')" />
      <Setter Property="Background" Value="#FFFF3232" />
    </Style>
    <Style TargetType="Node" GroupLabel="Model" ValueLabel="True">
      <Condition Expression="HasCategory('Model')" />
      <Setter Property="Background" Value="#FFFFFFFF" />
    </Style>
  </Styles>
</DirectedGraph>

 

View DbContext Model DDL SQL

若選擇Visual Studio 2019開發工具「方案總管」中的專案名稱,按一下滑鼠右鍵,從快捷選單中,選取「EF Core Power Tools」-「View DbContext Model DDL SQL」選項,請參考下圖所示:

clip_image034

圖 17:View DbContext Model DDL SQL。

接著在專案中便會根據目前DbContext模型來產生一個SQL檔案,描述要建立的資料庫結構,以本例來說,產生以下CREATE語法程式碼:

CREATE TABLE [Region] (

[RegionID] int NOT NULL,

[RegionDescription] nchar(50) NOT NULL,

CONSTRAINT [PK_Region] PRIMARY KEY NONCLUSTERED ([RegionID])

);

GO

「View DbContext Model DDL SQL」功能執行結果,請參考下圖所示:

clip_image036

圖 18:「View DbContext Model DDL SQL」功能執行結果。

View DbContext Model as DebugView

若選擇Visual Studio 2019開發工具「方案總管」中的專案名稱,按一下滑鼠右鍵,從快捷選單中,選取「EF Core Power Tools」-「View DbContext Model as DebugView」選項,請參考下圖所示:

clip_image038

圖 19:「View DbContext Model as DebugView」選項。

將會產生一個文字檔顯示在編輯畫面,其中描述模型的Metadata,以方便程式設計師來了解模型,以及幫助除錯。請參考以下檔案內容的列表:

Model:
  EntityType: Region
    Properties:
      RegionId (int) Required PK AfterSave:Throw
        Annotations:
          Relational:ColumnName: RegionID
          TypeMapping: Microsoft.EntityFrameworkCore.Storage.IntTypeMapping
      RegionDescription (string) Required MaxLength50
        Annotations:
          MaxLength: 50
          Relational:IsFixedLength: True
          TypeMapping: Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerStringTypeMapping
    Keys:
      RegionId PK
        Annotations:
          SqlServer:Clustered: False
    Annotations:
      ConstructorBinding: Microsoft.EntityFrameworkCore.Metadata.ConstructorBinding
      Relational:TableName: Region
Annotations:
  ProductVersion: 3.1.1
  Relational:MaxIdentifierLength: 128
  SqlServer:ValueGenerationStrategy: IdentityColumn

 

在使用Visual Studio 工具除錯時,也可以在中斷模式,從除錯視窗檢視這些資訊,請參考下圖所示,展開「context」-「Model」-「DebugView」-「View」選項:

clip_image040

圖 20:除錯視窗。

點選放大鏡圖示就會開啟「文字視覺化檢視」視窗,請參考下圖所示:

clip_image042

圖 21:顯示模型資訊。

Add AsDgml() extension method

若選擇Visual Studio 2019開發工具「方案總管」中的專案名稱,按一下滑鼠右鍵,從快捷選單中,選取「EF Core Power Tools」-「Add AsDgml() extension method」選項,請參考下圖所示:

clip_image044

圖 22:「Add AsDgml() extension method」選項。

選擇「Add AsDgml() extension method」選項會自動在專案中安裝一個「ErikEJ.EntityFrameworkCore.DgmlBuilder」套件,可為DbContext類別新增一個「AsDgml()」擴充方法,同時開發工具會顯示一個暫存的文字檔案,其中包含以下讀我內容,提供參考範例程式碼來產生dbml檔案:

** ErikEJ.EntityFrameworkCore.DgmlBuilder Readme **

To use the extension method to generate a DGML file of your DbContext model,
use code similar to this:
   
    using Microsoft.EntityFrameworkCore;
 

    using (var myContext = new MyDbContext())
    {
        System.IO.File.WriteAllText(System.IO.Path.GetTempFileName() + ".dgml",
            myContext.AsDgml(),
            System.Text.Encoding.UTF8);
    }

 

讓我們修改主控台應用程式的「Main」方法如下:

using EFPTDemo.Data;
using Microsoft.EntityFrameworkCore;
using System;

namespace EFPTDemo {
  class Program {
    static void Main( string[] args ) {
      using ( var myContext = new NorthwindContext() ) {
        string file = System.IO.Path.GetTempFileName() + ".dgml";
        Console.WriteLine(file); //C:\Users\UserName\AppData\Local\Temp\tmp2CAF.tmp.dgml
        System.IO.File.WriteAllText( file , myContext.AsDgml() ,System.Text.Encoding.UTF8 );
      }
    }
  }
}

執行程式之後,就會在指定的資料夾產生dbml檔案。

View Database Schema as Graph

若選擇Visual Studio 2019開發工具「方案總管」中的專案名稱,按一下滑鼠右鍵,從快捷選單中,選取「EF Core Power Tools」-「View Database Schema as Graph」選項,請參考下圖所示:

clip_image046

圖 23:「View Database Schema as Graph」選項。

下一步是連接到資料庫,由於本範例是以「Entity Framework Core 3.1.x」版,需在「Choose Database Connection」對話盒,勾選「Use EF Core 3.0」核取方塊,然後按一下「Add」按鈕,請參考下圖所示:

clip_image047

圖 24:連接到資料庫。

在「Select Tables to Script」對話盒,勾選要使用的資料表(可以選取多個),在此選取「Categories」與「Products」資料表,然後按下「OK」按鈕,請參考下圖所示:

clip_image049

圖 25:勾選要使用的資料表。

接下來就可以看到Model Diagram,請參考下圖所示,點選向下的箭頭可以展開群組資訊:

clip_image051

圖 26:Model Diagram。

接著在圖型介面中,便可以看到更詳細的資料表欄位資訊,請參考下圖所示:

clip_image053

圖 27:資料表欄位資訊。


Entity Framework Core Power Tools - 2

$
0
0

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

本篇文章延續《Entity Framework Core Power Tools - 1》一文的內容,介紹「Entity Framework Core Power Tools」套件,除了提供視覺化的介面來進行逆向工程(Reverse engineering)之外,還提供了哪些好用的功能。

Migration Tools

「EF Core Power Tools」也提供圖型介面來進行Code First 移轉 -「Migration Tools」。在本文撰寫時,此「Migration Tools」工具尚在預覽版,因此時不時會跳出錯誤訊息,不過只要多試幾次相同的操作,一樣可以完成這些操作動作。

我們來看一下做法,先建立一個新主控台應用程式。從Visual Studio 2019開發工具「檔案」-「新增」-「專案」項目,在「建立新專案」對話盒中,第一個下拉式清單方塊選取「C#」程式語言;從第二個下拉式清單方塊選取「所有平台」;從第三個下拉式清單方塊選取「主控台」,然後選取下方的「主控台應用程式(.NET Core)」範本。請參考下圖所示:

clip_image002

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

在「設定新的專案」對話盒中,設定專案名稱與儲存位置,然後按下「建立」按鈕,請參考下圖所示:

clip_image004

圖 2:「設定新的專案」對話盒。

從「方案總管(Solution Explorer)」視窗 – 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「管理NuGet套件」項目,從對話盒上方文字方塊中,輸入查詢關鍵字「Microsoft.EntityFrameworkCore.SqlServer」,找到「Microsoft.EntityFrameworkCore.SqlServer」套件3.1.x版後,點選「安裝」按鈕進行安裝,請參考下圖所示:

clip_image006

圖 3:安裝NuGet套件。

從「方案總管(Solution Explorer)」視窗 –專案名稱上方,按滑鼠右鍵,從快捷選單選擇「加入(Add)」- 「類別(Class)」項目,從「新增項目(Add New Item)」對話盒中,選取「Visual C#項目」-「類別(Class)」項目,然後在下方將「名稱(Name)」設定為「Regions」最後按下「新增(Add)」按鈕,請參考下圖所示:

clip_image008

圖 4:新增類別。

加入「Region」類別程式碼如下:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;

namespace EFMigrationDemo {

  public class Region {
    [Key]
    [Column( "RegionID" )]
    public int RegionId { get; set; }
    [Required]
    [StringLength( 50 )]
    public string RegionDescription { get; set; }
  }
}

 

重複上個步驟,加入一個新類別,名為「MyDbContext」,請參考下圖所示:

clip_image010

圖 5:加入「MyDbContext」類別。

在「MyDbContext」類別加入以下程式碼,其中設定了連接到SQL Server Express資料庫伺服器,MyDbContext資料庫的連接字串「Data Source=.\\sqlexpress;Initial Catalog=MyDbContext;Integrated Security=True」:

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace EFMigrationDemo {
  public class MyDbContext : DbContext {
    public MyDbContext() {

    }
    public MyDbContext( DbContextOptions<MyDbContext> options )
        : base( options ) {
    }

    public virtual DbSet<Region> Regions { get; set; }

    protected override void OnConfiguring( DbContextOptionsBuilder optionsBuilder ) {
      if ( !optionsBuilder.IsConfigured ) {
        optionsBuilder.UseSqlServer( "Data Source=.\\sqlexpress;Initial Catalog=MyDbContext;Integrated Security=True" );
      }
    }

    protected override void OnModelCreating( ModelBuilder modelBuilder ) {
      modelBuilder.Entity<Region>( entity => {
        entity.HasKey( e => e.RegionId )
            .IsClustered( false );

        entity.Property( e => e.RegionId ).ValueGeneratedNever();

        entity.Property( e => e.RegionDescription ).IsFixedLength();
      } );

    }

  }
}

選擇Visual Studio 2019開發工具「方案總管(Solution Explorer)」中的專案名稱,按一下滑鼠右鍵,從快捷選單中,選取「EF Core Power Tools」-「Migrations Tools (preview)」選項,請參考下圖所示:

clip_image012

圖 6:「Migrations Tools (preview)」選項。

在「Manage Migrations in Project 專案名稱」對話盒的文字方塊中,輸入「Migration Name」為「initial」(可以自訂),然後按下「Add Migration」按鈕,請參考下圖所示:

clip_image014

圖 7:「Manage Migrations in Project 專案名稱」對話盒。

在「Manage Migrations in Project 專案名稱」對話盒按一下「Update Databae」按鈕,請參考下圖所示:

clip_image016

圖 8:「Update Databae」按鈕。

這樣就大功告成啦,我們來檢視一下這個步驟產生的資料庫結構描述資訊。開啟Visual Studio 2019開發工具「伺服器總管(Server Explorer)」視窗,在「資料連接(Data Connections)」項目上按滑鼠右鍵,從快捷選單之中選取「加入連接(Add Connection)」項目,請參考下圖所示:

clip_image018

圖 9:「加入連接(Add Connection)」。

連接到微軟SQL Server Express 2019,在「加入連接(Add Connection)」視窗中,設以下屬性,請參考下圖所示:

· 資料來源 (Data Source) :Microsoft SQL Server (SqlClient)。

· 伺服器名稱(Server name)欄位:輸入「.\SQLExpress」。

· 驗證(Authentication):選取「Windows驗證(Windows Authentication)」。

· 選取或輸入資料庫名稱(Select or enter a database name)欄位:選擇「MyDbContext」資料庫。

clip_image020

圖 10:連接到資料庫。

檢視新建立的資料表與欄位資訊,請參考下圖所示:

clip_image022

圖 11:新建立的資料表與欄位資訊。

試著新增一筆資料到資料表。在「Program」類別「Main」方法加入以下程式碼:

using System;

namespace EFMigrationDemo {
  class Program {
    static void Main( string[] args ) {
      using ( MyDbContext context = new MyDbContext() ) {
        context.Add( new Region() { RegionId = 1 , RegionDescription = "Eastern" } );
        context.SaveChanges();
        foreach ( var item in context.Regions ) {
          Console.WriteLine( $"Region Id : {item.RegionId} , Region Description : {item.RegionDescription}" );
        }
      }

    }
  }
}

 

這個範例程式的執行結果參考如下:

clip_image024

圖 12:新增並查詢資料。

發行與部署.NET Core應用程式

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N200622001
出刊日期: 2020/6/10

一旦應用程式設計完成了,我們需要將它們部署到其它機器上運行,.NET Core提供了幾種部署方式,包含「與 Framework 相依的部署(Framework-dependent deployment,FDD)」、「自封式部署(Self-contained deployment,SCD)」。你可以利用Visual Studio 2019開發工具,或是利用CLI命令來打包發行設計好的.NET Core應用程式,在這篇文章中,我們將來了解一下使用Visual Studio 2019來進行發行與部署。

建立測試專案

讓我們先啟動Visual Studio 2019開發環境,建立測試專案。從Visual Studio開發工具「File」-「New」-「Project」項目,在「Create a New Project」對話盒中,選取「C#」程式語言,選取「Console App(NET Core)」,請參考下圖所示:

clip_image002

圖 1:建立「Console App(NET Core)」。

在「Configure your new project」視窗中設定專案名稱為「ConsoleApp1」,設定專案存放路徑,然後按下「Create」鍵,請參考下圖所示:

clip_image004

圖 2:「Configure your new project」視窗。

預設專案中包含一個「Program.cs」檔案,其中包含程式碼印出「Hello World!」訊息到主控台:

Program.cs

using System;

namespace ConsoleApp1 {
  class Program {
    static void Main( string[] args ) {
      Console.WriteLine( "Hello World!" );
    }
  }
}

 

編譯這個專案,選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯,然後按CTRL+F5執行程式,執行結果參考如下圖所示:

clip_image006

圖 3:主控台應用程式。

接著讓我們使用Visual Studio 2019發行專案。

使用Visual Studio 2019發行專案

使用Visual Studio 發行.NET Core專案的方式又細分為以下幾種:

· 與 Framework 相依的部署(Framework-dependent deployment)。

· 有協力廠商相依性的 Framework 相依部署(Framework-dependent deployment)。

· 自封式部署(Self-contained deployment)。

· 有協力廠商相依性的自封式部署(Self-contained deployment)。

與 Framework 相依的部署(Framework-dependent deployment)

就名稱所暗示,「與 Framework 相依的部署(Framework-dependent deployment)」將依賴目的電腦需要事先安裝好.NET Core runtime,專案發行的結果只需要包含你的應用程式相關程式碼。如此的好處是.NET Core runtime可以讓多個不同的應用程式共享。

從「Solution Explorer」視窗,「ConsoleApp1」專案名稱上方按滑鼠右鍵,從快捷選單選擇「Publish」選項,請參考下圖所示:

clip_image008

圖 4:發行專案。

在「Pick a publish target」視窗中,選取發行到「Folder」,然後點選「Create Profile」按鈕

,請參考下圖所示:

clip_image010

圖 5:發行到資料夾。

在「Publish」發行視窗中設定以下項目:

· 「Target Locaiton」 :發行結果要輸出的資料夾。

· 「Configuration」:使用「Release」組態模式來發行。

· 「Target Framework」:設定「目標Framework」為「.netcoreapp 3.1」。

· 「Target Runtime」:設定為「Portable」,發行結果可運行在相容的機器上。例如.NET Core 2.0之後將Linux視為一個單獨的作業系統,編譯成「Portable」可適用於所有Linux環境中。

專案預設部署模式為「與 Framework 相依的部署(Framework-dependent deployment)」,我們直接點選「Publish」按鈕進行發行,請參考下圖所示:

clip_image012

圖 6:發行「Portable」。

完成後,透過檔案總管檢視「publish」資料夾,得到以下檔案清單,請參考下圖所示:

clip_image014

圖 7:「publish」資料夾。

「ConsoleApp1.pdb」檔案為程式資料庫(Program Database),包含應用程式的除錯相關資訊,協助除錯應用程式的例外錯誤。若無除錯需求,這個檔案可以不必部署到目的地電腦中。

發行的資料夾內會產生一個平台專屬的執行檔(platform-specific executable,.exe):「ConsoleApo1.exe」,也稱之為平台相依執行檔(framework-dependent executable),此執行檔(*.exe)不能夠跨平台,專屬於特定作業系統與CPU架構,「與 Framework 相依的部署(Framework-dependent deployment)」方式可以不需要這個執行檔(.exe),後續可以透過「dotnet.exe」來執行跨平臺二進位(cross-platform binary)檔(即附檔名為「*.dll」的檔案),這種部署方式是跨平台的。

跨平臺二進位(cross-platform binary)檔預設使用專案名稱命名,例如我們專案名稱為「ConsoleApp1」,產生的跨平臺二進位(cross-platform binary)檔名便為「ConsoleApp1.dll」。跨平臺二進位(cross-platform binary)檔可以在任何有安裝特定版本的Target Framework上執行(例如本文範例的「netcoreapp3.1」),若目地電腦沒有安裝此版本的.NET Runtime,則會找更新的版本來執行。

發行的結果中包含一個「*.deps.json」檔案,這個檔案記錄了相依組件的清單:

ConsoleApp1.deps.json

{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v3.1",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v3.1": {
      "ConsoleApp1/1.0.0": {
        "runtime": {
          "ConsoleApp1.dll": {}
        }
      }
    }
  },
  "libraries": {
    "ConsoleApp1/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    }
  }
}

 

發行的結果中包含一個「ConsoleApp1.runtimeconfig.json」檔案,指定要使用共用的Framework :「Microsoft.NETCore.App」來執行程式:

ConsoleApp1.runtimeconfig.json

{
  "runtimeOptions": {
    "tfm": "netcoreapp3.1",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "3.1.0"
    }
  }
}

 

好了,現在你只需要將「publish」資料夾中的檔案複製到目地電腦,目的地電腦要事先裝好.NET Core Runtime,以Windows作業系統為例,直接執行ConsoleApp1.exe平台專屬的執行檔(platform-specific executable),請參考下圖所示:

clip_image016

圖 8:執行ConsoleApp1.exe平台專屬的執行檔(platform-specific executable)。

或只是使用「dotnet」來執行跨平臺二進位(cross-platform binary)檔,請參考下圖所示:

clip_image018

圖 9:使用「dotnet」執行跨平臺二進位(cross-platform binary)檔。

設定執行階段識別碼(Runtime Identifier ,RID)

不管使用「與 Framework 相依的部署(Framework-dependent deployment)」或後面要談的「自封式部署(Self-contained deployment)」,發行的資料夾內會產生一個平台專屬的執行檔(.exe),你可以在發行時指定執行階段識別碼(Runtime Identifier ,RID),來指定想要使用的平台,例如「win-x64」、「win-x86」、「linux-x64」等等,完整的執行階段識別碼清單可以參閱微軟官方網站:「https://docs.microsoft.com/zh-tw/dotnet/core/rid-catalog」。

舉例來說,若要設定執行階段識別碼(Runtime Identifier ,RID),在「Publish」視窗中,選取「Edit」項目,請參考下圖所示:

clip_image020

圖 10:設定執行階段識別碼(Runtime Identifier ,RID)。

在「Profile Settings」視窗中設定「Target Runtime」,請參考下圖所示:

clip_image022

圖 11:設定「Target Runtime」指定RID。

自封式部署(Self-contained deployment)

「自封式部署(Self-contained deployment)」會將你應用程式程式碼、.NET Core程式庫、.NET Core Runtime打包在一起,目地電腦不必事先裝.NET Core。如此的好處是一台機器上,可以有多個程式,在不同版本的.NET Core環境中並行運行。

讓我們試著將「Deployment Mode」設定為「Self-contained」,再進行主控台應用程式的發行,請參考下圖所示:

clip_image024

圖 12:自封式部署(Self-contained deployment)。

檢視「publish」資料夾,其中共有226個檔案,除了專案程式編譯完的組件之外,還包含.NET Core Runtime等共用程式庫,這些檔案可以直接複製到沒有安裝.NET Core Runtime的Windows機器上執行,請參考下圖所示:

clip_image026

圖 13:自封式部署(Self-contained deployment)。

有協力廠商相依性的 Framework 相依部署(Framework-dependent deployment)

若應用程式與協力廠商的組件有相依性,那麼發行的結果有哪些不同呢? 讓我們修改一下主控台應用程式的程式碼,使用很流行的Json.NET函式庫來進行物件序列化與還原序列化動作。先使用Nuget套件管理員下載Json.NET函式庫。選取「Solution Explorer」視窗-「ConsoleApp1」專案。從Visual Studio開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在提示字元中輸入install-package指令:

install-package Newtonsoft.Json

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

clip_image028

圖 14:使用Nuget套件管理員安裝Json.NET函式庫。

檢視專案檔案包含「PackageReference」項目,描述了相依性,請參考以下程式碼:

ConsoleApp1.csproj

 

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
  </ItemGroup>

</Project>

 

修改「Program.cs」檔案,加入以下程式碼,利用「JsonConvert」類別序列化「List<Book>」集合,並就序列化的結果,再做還原序列化:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
namespace ConsoleApp1 {
  public class Book {
    public int Id { get; set; }
    public string Title { get; set; }
    public int Price { get; set; }
    public DateTime PublishDate { get; set; }
    public bool InStock { get; set; }
    public string Description { get; set; }
    public Category? Category { get; set; }
  }
  public enum Category {
    Arts, Business, Commics, Cooking, Computers, History, Literature, Sports, Travel
  }
  class Program {

    static void Main( string[] args ) {

      List<Book> _books = new List<Book>() {
         new Book() {
           Id = 1 ,
           Title = " Essential Programming Language " ,
           Price = 250 ,
           PublishDate = new DateTime( 2019 ,1,2 ) ,
           InStock = true ,
           Description = "Essential Programming Language "  ,
          Category = Category.Computers
         },
         new Book() {
           Id = 2 ,
           Title = " Telling Arts " ,
           Price = 245 ,
           PublishDate = new DateTime( 2019 , 4 , 15 ) ,
           InStock = true ,
           Description = " Telling Arts "  ,
          Category = Category.Arts
         },
           new Book() {
           Id = 3 ,
           Title = " Marvel " ,
           Price = 150  ,
           PublishDate = new DateTime( 2019 , 2, 21 ) ,
           InStock = true ,
           Description = " Marvel "  ,
          Category = Category.Commics
         },
          new Book() {
           Id = 4 ,
           Title = " The Beauty of Cook" ,
           Price = 450 ,
           PublishDate = new DateTime( 2019 ,12,2 ) ,
           InStock = true ,
           Description = " The Beauty of Cook "  ,
           Category = Category.Cooking
         }
      };

      string jsonText = JsonConvert.SerializeObject( _books , Formatting.Indented);
      Console.WriteLine( jsonText );
      Console.WriteLine();
      List<Book> jsonObjList = JsonConvert.DeserializeObject<List<Book>>( jsonText );
      Console.WriteLine( "Book list : " );
      foreach ( var item in jsonObjList ) {
        Console.WriteLine( $" {item.Id} , {item.Title} , {item.Price} , {item.InStock} , {item.Category}" );
      }
    }
  }
}

 

選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行,執行的結果請參考下圖所示:

clip_image030

圖 15:使用「JsonConvert」類別序列化與還原序列化。

我們先試試「有協力廠商相依性的 Framework 相依部署(Framework-dependent deployment)」,請參考下圖在「Profile Settings」視窗,進行以下設定,再進行發行:

clip_image032

圖 16:有協力廠商相依性的 Framework 相依部署(Framework-dependent deployment)。

發行之後,檢視「publish」資料夾,Visual Studio會自動將專案相依的「Newtonsoft.json.dll」加到資料夾之中,請參考下圖所示:

clip_image034

圖 17:有協力廠商相依性的 Framework 相依部署(Framework-dependent deployment)。

此外檢視「ConsoleApp1.deps.json」檔案,也會紀錄應用程式使用到的相依組件「Newtonsoft.Json」,請參考以下程式碼:

ConsoleApp1.deps.json

{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v3.1/win-x64",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v3.1": {},
    ".NETCoreApp,Version=v3.1/win-x64": {
      "ConsoleApp1/1.0.0": {
        "dependencies": {
          "Newtonsoft.Json": "12.0.3"
        },
        "runtime": {
          "ConsoleApp1.dll": {}
        }
      },
      "Newtonsoft.Json/12.0.3": {
        "runtime": {
          "lib/netstandard2.0/Newtonsoft.Json.dll": {
            "assemblyVersion": "12.0.0.0",
            "fileVersion": "12.0.3.23909"
          }
        }
      }
    }
  },
  "libraries": {
    "ConsoleApp1/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "Newtonsoft.Json/12.0.3": {
      "type": "package",
      "serviceable": true,
      "sha512": "sha512-6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==",
      "path": "newtonsoft.json/12.0.3",
      "hashPath": "newtonsoft.json.12.0.3.nupkg.sha512"
    }
  }
}

有協力廠商相依性的自封式部署(Self-contained deployment)

最後試試「有協力廠商相依性的自封式部署(Self-contained deployment)」,請參考下圖在「Profile Settings」視窗,進行以下設定,再進行發行:

clip_image036

圖 18:「有協力廠商相依性的自封式部署(Self-contained deployment)」。

發行之後,檢視「publish」資料夾,Visual Studio同樣會自動將專案相依的「Newtonsoft.json.dll」加到資料夾之中,請參考下圖所示:

clip_image038

圖 19:「有協力廠商相依性的自封式部署(Self-contained deployment)」。

產生單一檔案 (Produce a single file)

.NET Core 3.x版新增一個發行選項,請參考下圖所示,能夠將發行結果,包含你的應用程式、相依程式,與.NET Core runtime等等全部打包成一個可執行檔案,只要在「Profile Settings」視窗,勾選「Produce a single file」,請參考下圖所示:

clip_image040

圖 20:產生單一檔案 (Produce a single file)。

發行完,檢視「publish」資料夾,其中只包含兩個檔案,Visual Studio 2019會將相依檔案全部打包在同一個單一執行檔之中,檔案的大小為「68162KB」,請參考下圖所示:

clip_image042

圖 21:產生單一檔案 (Produce a single file)- 「Self - contained」。

讓我們改設定為「Framework Dependent」再進行發行,請參考下圖所示:

clip_image044

圖 22:產生單一檔案 (Produce a single file)- 「Framework Dependent」。

發行後,檔案的大小為「852KB」,比設定為「Self - contained 」發行要小很多,請參考下圖所示:

clip_image046

圖 23產生單一檔案 (Produce a single file)- 「Framework Dependent」。

這個執行檔內將含執行應用程式所有相依程式,第一次執行時會自我解壓縮到暫存資料夾,下次再執行時,就不需要再進行一次解壓縮的動作。

修剪未使用的組件(Trim unused assemblies)

自封式部署(Self-contained deployment)還有一個額外的「修剪未使用的組件(Trim unused assemblies」功能,能將未使用到的程式移出輸出的組件。在發行時勾選「Trim unused assemblies」,請參考下圖所示:

clip_image048

圖 24:修剪未使用的組件(Trim unused assemblies)。

發行後,檢視檔案大小比沒有勾選「修剪未使用的組件(Trim unused assemblies)」選項的發行方式少了一半,約「35,935KB」,請參考下圖所示:

clip_image050

圖 25:修剪未使用的組件(Trim unused assemblies)。

啟用ReadyToRun編譯

.NET Core 3.x版之後,多了一個「啟用ReadyToRun編譯(Enable Ready ToRun compilication)」選項搭配發行使用,可以將程式編譯成ReadyToRun (R2R) 格式。勾選「啟用ReadyToRun編譯(Enable Ready ToRun compilication)」選項後發行的結果將包含原生程式碼(Native Code),可以改善應用程式啟動的時間。

clip_image052

圖 26:啟用ReadyToRun編譯(Ready to Run)。

勾選「啟用ReadyToRun編譯(Enable Ready ToRun compilication)」選項後發行結果的檔案較上個設定大一些,請參考下圖所示,這是因為其中同時包含了原生程式與中介(IL)程式。

clip_image054

圖 26:啟用ReadyToRun編譯(Ready to Run)。

總結

「與 Framework 相依的部署(Framework-dependent deployment)」的特色在於:

  • 部署的檔案小,只有你的應用程式與相依組件需要複製到目地電腦。NET Core Runtime要事先安裝到目地電腦,可以讓多個應用程式共享。
  • 程式可以跨平台。
  • 目的電腦的.NET Runtime更新時(主要、次要版本)會自動使用新版的.NET Runtime來執行。

「自封式部署(Self-contained deployment)」的特色在於:

  • 可以控制應用程式使用的.NET Core版本。多個.NET Core版本可以並存在一台機器上。
  • 部署的檔案較大。

發行與部署.NET Core應用程式 - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N200622002
出刊日期: 2020/6/24

在《發行與部署.NET Core應用程式》一文中,介紹.NET Core主控台應用程式(Console Applicaiton)發行與部署的細節,本文將延續這個主題,看看在Visual Studio 2019發行ASP.NET Core MVC應用程式的設定與結果。

建立ASP.NET Core網站應用程式專案

首先我們來看看,如何使用「與 Framework 相依的部署(Framework-dependent deployment)」來發行ASP.NET Core網站應用程式範本專案。啟動Visual Studio 2019開發環境。從Visual Studio開發工具「File」-「New」-「Project」項目,在「Create a New Project」對話盒中,選取「ASP.NET Core Web Application」範本。請參考下圖所示:

clip_image002

圖 1:建立ASP.NET Core 應用程式專案。

在「Configure your new project」對話盒中,設定專案名稱與專案存放路徑,然後按下「Create」鍵,請參考下圖所示:

clip_image004

圖 2:設定專案名稱與專案存放路徑。

在「Create a new ASP.NET Core Web Application」對話盒中,確認左上方的清單選取「.NET Core」,右上方的清單ASP.NET Core版本為「ASP.NET Core 3.x」,選取下方的「Web Application (Model – View - Controller」樣版專案,勾選「Configure for HTTPS」、清除勾選「Enable Docker Support」核取方塊,確定右方的「Authentication」項目設定為「No Authentication」,然後按下「Create」按鈕建立專案,請參考下圖所示:

clip_image006

圖 3:選取下方的「Web Application (Model – View - Controller)」樣版專案。

選取Visual Studio 2019開發工具「Build」-「Build Solution」項目編譯目前的專案。接著按「CTRL」+「F5」組合鍵來執行網站應用程式,此時會啟動瀏覽器,以及開發用的IIS Express伺服器,自動指定一個埠號(port)來執行網站應用程式。執行結果參考如下:

clip_image008

圖 4:執行範本專案。

使用Visual Studio 2019發行專案

從「Solution Explorer」視窗 ,「MyWebApp」專案名稱上方按滑鼠右鍵,從快捷選單選擇「Publish」選項,請參考下圖所示:

clip_image010

圖 5:發行專案。

在「Pick a publish target」視窗中,選取「Folder」發行到指定的資料夾,請參考下圖所示:

clip_image012

圖 6:發行到資料夾。

點選「Pick a publish target」視窗中的「Advanced...」,可以進一步設定發行的細節,請參考下圖所示:

clip_image014

圖 7:發行「Portable」。

在「Publish」發行視窗中設定以下項目,然後按下「Save」按鈕:

· 「Configuration」:使用「Release」組態模式來發行。

· 「Target Framework」:設定「目標Framework」為「.netcoreapp 3.1」。

· 「Target Locaiton」 :發行結果要輸出的資料夾。

· 「Deployment Mode」:專案預設部署模式為「與 Framework 相依(Framework-dependent)」

· 「Target Runtime」:設定為「Portable」,發行結果可運行在相容的機器上。

· 勾選「Remove additional files at destination」項目,如此每次發行時,不需要手動刪除前一次部署的檔案。

回到「Pick a publish target」視窗,然後點選「Create Profile」按鈕,請參考下圖所示:

clip_image016

圖 8:建立Profile。

在「Publish」發行視窗中,直接點選「Publish」按鈕進行發行,請參考下圖所示:

clip_image018

圖 9:發行專案。

發行的資料夾內會產生一個平台專屬的執行檔(platform-specific executable,MyWebApp.exe),以及跨平臺二進位檔(cross-platform binary,MyWebApp.dll與MyWebApp.Views.dll),不含「wwwroot」資料夾中的靜態檔案,所有檔案加起來也只有幾百KB大小。

clip_image020

圖 10:專案發行的結果。

若要測試網站應用程式發行的結果是否能正常運作,可以開啟命令提示字元,執行發行資料夾中的「MyWebApp.exe」檔案,預設會啟動網站伺服器,接聽「5000」與「5001」埠,請參考下圖所示:

clip_image022

圖 11:測試發行結果。

開啟瀏覽器,輸入以下網址:

http://localhost:5000

看看瀏覽器是否能正常呈現網站應用程式,請參考下圖所示:

clip_image024

圖 12:測試發行的網站。

自封式部署(Self-contained deployment)

讓我們試著將「Deployment Mode」設定為「Self-contained」,再進行網站應用程式的發行,在「Publish」視窗中,選取「Edit」項目,在「Profile Settings」視窗中設定「Deployment Mode」為「Self-contained」自封式部署,並指定「Target Runtime」為「win-x64」,請參考下圖所示:

clip_image026

圖 13:「Deployment Mode」設定為「Self-contained」。

檢視「publish」資料夾,其中共有362個檔案,網站的程式碼將編譯在「MyWebApp.dll」與「MyWebApp.Views.dll」(檢視程式碼)之中,請參考下圖所示:

clip_image028

圖 14:自封式部署(Self-contained deployment)。

產生單一檔案 (Produce a single file)

.NET Core 3新增一個產生單一檔案 (Produce a single file)功能,能夠將發行結果,包含你的應用程式、相依程式,與.NET Core runtime等等全部打包成一個可執行檔案。在「Profile Settings」視窗,勾選「產生單一檔案 (Produce a single file)」,請參考下圖所示:

clip_image030

圖 15:產生單一檔案 (Produce a single file)。

發行完,檢視「publish」資料夾,Visual Studio 2019會將相依檔案全部打包在同一個單一執行檔之中,檔案的大小為「84.7MB」,請參考下圖所示:

clip_image032

圖 16:產生單一檔案 (Produce a single file)。

修剪未使用的組件(Trim unused assemblies)

「修剪未使用的組件(Trim unused assemblies)」也是.NET Core 3新增的功能,但目前還在預覽版(Preview)。在發行時勾選「修剪未使用的組件(Trim unused assemblies)」,將未使用到的程式移出輸出的組件,請參考下圖所示:

clip_image034

圖 17:修剪未使用的組件(Trim unused assemblies)。

發行後,檢視檔案大小比沒有勾選「修剪未使用的組件(Trim unused assemblies)」選項的發行方式少了一半,約「44.7MB」,請參考下圖所示:

clip_image036

圖 18:修剪未使用的組件(Trim unused assemblies)。

啟用ReadyToRun編譯

「啟用ReadyToRun編譯(Enable Ready ToRun compilication)」選項,可以將程式編譯成ReadyToRun (R2R) 格式,發行的結果將包含原生程式碼(Native Code),可以改善應用程式啟動的時間。

clip_image038

圖 19:啟用ReadyToRun編譯(Ready to Run)。

勾選「啟用ReadyToRun編譯(Enable Ready ToRun compilication)」選項後發行,發行結果的檔案較上個設定大一些,請參考下圖所示,這是因為其中同時包含了原生程式與中介(IL)程式。

clip_image040

圖 20:啟用ReadyToRun編譯(Ready to Run)。

Razor Page入門 - 5

$
0
0

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

這篇文章將延續《Razor Page入門 - 4》一文的情境,介紹當在ASP.NET Core Razor Page網站應用程式中如何使用「System.ComponentModel.DataAnnotations」命名空間下的類別來進行語意標註,並可在資料編輯作業時利用模型驗證(Model Validation)檢查資料的正確性。

「System.ComponentModel.DataAnnotations」命名空間下常用的Attribute類別包含以下:

  • 「Display」 Attribute:設定顯示名稱,並非用來進行資料驗證。
  • 「Required」 Attribute:要求驗證資料必須輸入。
  • 「MaxLength」 Attribute:來設定資料最大長度。
  • 「Range」Attribute:設定資料範圍,可指定資料最小、最大值。

安裝「Microsoft.AspNetCore.Mvc.DataAnnotations」套件

本文範例的模型類別是放在一個名為「MyModels」的.NET Standard類別庫(.NET Standard Class Library),你需要先安裝「Microsoft.AspNetCore.Mvc.DataAnnotations」套件才能夠在專案中使用「System.ComponentModel.DataAnnotations」命名空間下的Attribute類別。

從「Solution Explorer」視窗 –「MyModels」專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Manage NuGet Packages」項目,請參考下圖所示:

clip_image002

圖 1:「Manage NuGet Packages」項目。

從對話盒上方文字方塊中,輸入查詢關鍵字「DataAnnotations」,找到「Microsoft.AspNetCore.Mvc.DataAnnotations」套件後,點選「Install」按鈕進行安裝,請參考下圖所示:

clip_image004

圖 2:安裝「Microsoft.AspNetCore.Mvc.DataAnnotations」套件。

下一步會看到「Preview Changes」視窗(預覽變更視窗),按一下「OK」按鈕,請參考下圖所示:

clip_image006

圖 3:「Preview Changes」視窗。

按一下「License Acceptance」視窗中的「I Accept」按鈕,請參考下圖所示:

clip_image008

圖 4:「License Acceptance」視窗。

接著就會進行安裝的動作,完成後專案相依的套件與版本會紀錄在「MyModels.csproj」專案檔之中,參考以下程式碼:

MyModels.csproj

 

<Project Sdk = "Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework> netstandard2.0 </TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include = "Microsoft.AspNetCore.Mvc.DataAnnotations" Version = "2.2.0" />
  </ItemGroup>

</Project>



 

「Microsoft.AspNetCore.Mvc.DataAnnotations」套件安裝完成之後,我們就可以在模型類別之中使用到語意標註相關類別。修改「MyModels\Book.cs」檔案中的「Book」類別,加入以下程式碼:

MyModels\Book.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;

namespace MyModels {
  public class Book {
    [Display( Name = "圖書編號" )]
    public int Id { get; set; }
    [Display( Name = "圖書名稱" )]
    [Required( ErrorMessage = "圖書名稱不可為空白" )]
    [MaxLength( 50 , ErrorMessage = "長度不可超過 {0}" )]
    public string Title { get; set; }
    [Display( Name = "價格" )]
    [Range( 1 , int.MaxValue , ErrorMessage = "{0} 有效範圍在 {1} 與 {2} 之間" )]
    public int Price { get; set; }
    [Display( Name = "出版日期" )]
    public DateTime PublishDate { get; set; }
    [Display( Name = "庫存" )]
    public bool InStock { get; set; }
    [Display( Name = "說明" )]
    [MaxLength( 50 , ErrorMessage = "長度不可超過 {0}" )]
    public string Description { get; set; }
    [Display( Name = "圖書分類" )]
    public Category? Category { get; set; }
  }

}


 

我們修改了「Book 」類別程式碼,使用Data Annotation加上以下語意標註,使用「DisplayName 」Attribute設定顯示名稱;「Title」屬性套用「Required」 Attribute要求資料必需輸入;「MaxLength」 Attribute設定資料最大長度;「Price」屬性套用「Range」 Attribute要求資料的有效範圍是正整數。若資料驗證有錯,再透過這些Attribute類別的「ErrorMessage 」屬性來自訂錯誤訊息。

選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。

在Razor Page顯示錯誤驗證訊息

回到「MyRazorWeb」網站專案,修改「\Edit.cshtml」Razor Page程式碼,顯示資料驗證錯誤訊息。在<form>標籤內加入一個<div>標籤,套用「asp-validation-summary」標記協助程式(Tag Helper),以集中顯示多個<Input>項目驗證錯誤時的錯誤訊息。此外在每一行<Input>標籤程式下方加入一個<span>標籤,設定「asp-validation-for」標記協助程式(Tag Helper)進行驗證:

MyRazorWeb\Pages\Books\Edit.cshtml

@page
@model MyRazorWeb.Pages.Books.EditModel
@{
    ViewData["Title"] = "Edit";
}

<h1> Book Edit </h1>
<hr />
<div class = "row">
    <div class = "col-md-8">
        <form method = "post">
            <div asp-validation-summary = "All" class = "text-danger"> </div>
            <input type = "hidden" asp-for = "Book.Id" />
            <div class = "form-group">
                <label asp-for = "Book.Title" class = "control-label"> </label>
                <input asp-for = "Book.Title" class = "form-control" />
                <span asp-validation-for = "Book.Title" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.Price" class = "control-label"> </label>
                <input asp-for = "Book.Price" class = "form-control" />
                <span asp-validation-for = "Book.Price" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.PublishDate" class = "control-label"> </label>
                <input asp-for = "Book.PublishDate" class = "form-control" />
                <span asp-validation-for = "Book.PublishDate" class = "text-danger"> </span>
            </div>
            <div class = "form-group form-check">
                <label class = "form-check-label">
                    <input class = "form-check-input" asp-for = "Book.InStock" /> @Html.DisplayNameFor( model => model.Book.InStock )
                </label>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.Description" class = "control-label"> </label>
                <input asp-for = "Book.Description" class = "form-control" />
                <span asp-validation-for = "Book.Description" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.Category" class = "control-label"> </label>
                <select asp-for = "Book.Category" class = "form-control"
                        asp-items = "Html.GetEnumSelectList<MyModels.Category>()">
                    <option value = ""> Please Select </option>
                </select>
                <span asp-validation-for = "Book.Category" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <input type = "submit" value = "Save" class = "btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page = "./List"> Back to List </a>
</div>

 

參考以下程式碼,接下來修改「\Edit.cshtml.cs」檔案程式碼,特別注意,在「Book」屬性套用了「BindProperty」Attribute,模型繫結程式將搜集到的資料填入Book物件的屬性值時,會根據模型類別屬性套用的語意標註Attribute(Data Annotations Attribute)驗證資料的有效性,只要資料有問題,就會將「ModelState.IsValid」屬性的值設定為「false」,若所有屬性都通過資料驗證,就會將「ModelState.IsValid」屬性的值設定為「true」:

MyRazorWeb\Pages\Books\Edit.cshtml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyModels;
using MyServices;

namespace MyRazorWeb.Pages.Books {
  public class EditModel : PageModel {
    private readonly IBookRepository bookRepository;
    public EditModel( IBookRepository bookRepository ) {
      this.bookRepository = bookRepository;
    }
    [BindProperty]
    public Book Book { get; set; }
    public IActionResult OnGet( int id ) {
      Book = bookRepository.GetBook( id );

      if ( Book == null ) {
        return RedirectToPage( "/NotFound" );
      }

      return Page();
    }
    public IActionResult OnPost() {
      if ( !ModelState.IsValid ) {
        return Page();
      }
      Book = bookRepository.Update( Book );
      return RedirectToPage( "List" );
    }
  }
}

 

因此我們在「OnPost」方法之中只要判斷驗證的結果是成功還是失敗再進行因應的處理就好,以本例而言,若驗證不成功,便直接叫用「Page()」方法,回傳「Edit」Razor Page,讓使用者修訂錯誤,以便再重新提交表單;若驗證沒有問題,便更新伺服端的資料,然後透過「RedirectToPage」方法導向「List」Razor Page。

選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站「Books\List」Razor Page,執行結果參考如下,點選某筆圖書後方的「Edit」超連結時,「Book」物件的「id」屬性值就以查詢字串的型式,傳遞到「Edit」Razor Page:

clip_image010

圖 5:錯誤驗證測試。

下一步便會進入編輯畫面,任意修改文字方塊中的值,再按下「Save」按鈕,請參考下圖所示,我們故意不填「Title(圖書名稱)」;並將「Price(價格)」設為負數值,錯誤訊息會自動出現在「asp-validation-summary」與「asp-validation-for」標記協助程式(Tag Helper)所在的位置:

clip_image012

圖 6:顯示錯誤訊息。

用戶端驗證

預設ASP.NET Core Razor Pages類型的專案已經將用戶端驗證所需的「jQuery」、「jquery-validation」與「jquery-validation-unobtrusive」三個JavaScript程式庫放在「wwwroot\lib」資料夾之中,請參考下圖所示:

clip_image014

圖 7:JavaScript程式庫。

要讓ASP.NET Core Razor Pages用戶端驗證能夠生效,只要在需要驗證的Razor Page引用這三個JavaScript程式庫即可。但每個需要使用到驗證的Razor Page都要重複做引用的動作在設計上稍嫌煩雜,也不易後續管理與維護,能夠集中處理的話是最好的選擇。

檢視一下目前專案中的「_Layout」檔案程式碼已經使用<script>標籤引用「jquery.min.js」程式庫,參考以下程式碼:

MyRazorWeb\Pages\Shared\_Layout.cshtml

<!DOCTYPE html>
<html lang = "en">
<head>
    <meta charset = "utf-8" />
    <meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
    <title> @ViewData["Title"] – MyRazorWeb </title>
    <link rel = "stylesheet" href = "~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel = "stylesheet" href = "~/css/site.css" />
</head>
<body>
    <header>
        <nav class = "navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class = "container">
                <a class = "navbar-brand" asp-area = "" asp-page = "/Index"> MyRazorWeb </a>
                <button class = "navbar-toggler" type = "button" data-toggle = "collapse" data-target = ".navbar-collapse" aria-controls = "navbarSupportedContent"
                        aria-expanded = "false" aria-label = "Toggle navigation">
                    <span class = "navbar-toggler-icon"> </span>
                </button>
                <div class = "navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class = "navbar-nav flex-grow-1">
                        <li class = "nav-item">
                            <a class = "nav-link text-dark" asp-area = "" asp-page = "/Index"> Home </a>
                        </li>
                        <li class = "nav-item">
                            <a class = "nav-link text-dark" asp-area = "" asp-page = "/Books/List"> Books </a>
                        </li>
                        <li class = "nav-item">
                            <a class = "nav-link text-dark" asp-area = "" asp-page = "/Privacy"> Privacy </a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class = "container">
        <main role = "main" class = "pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class = "border-top footer text-muted">
        <div class = "container">
            &copy; 2020 - MyRazorWeb - <a asp-area = "" asp-page = "/Privacy"> Privacy </a>
        </div>
    </footer>

    <script src = "~/lib/jquery/dist/jquery.min.js"> </script>
    <script src = "~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"> </script>
    <script src = "~/js/site.js" asp-append-version = "true"> </script>

    @RenderSection( "Scripts", required: false )
</body>
</html>

 

在專案中多個Razor Page可能都會使用到「jquery」程式庫,而考慮到只有需要驗證資料的「Edit」Razor Page以及未來要設計用來新增資料的「Create」Razor Page才會用到「jquery-validation」與「jquery-validation-unobtrusive」這兩個JavaScript程式庫,因此我們不在「_Layout」檔案中引用「jquery-validation」與「jquery-validation-unobtrusive」。

預設ASP.NET Core Razor Pages類型的專案在「MyRazorWeb\Pages\Shared\」資料夾之中,存放一個「_ValidationScriptsPartial.cshtml」檔案,包含了引用「jquery-validation」與「jquery-validation-unobtrusive」這兩個JavaScript程式庫的程式碼如下:

_ValidationScriptsPartial.cshtml

<script src = "~/lib/jquery-validation/dist/jquery.validate.min.js"> </script>
<script src = "~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"> </script>

 

參考以下程式碼,修改「Edit.cshtml」程式碼,利用「section」關鍵字定義一個「Scripts」區段,在其中叫用「Html.RenderPartialAsync」方法將「_ValidationScriptsPartial.cshtml」檔案中的程式碼插入「_Layout」檔案中「 @RenderSection("Scripts", required: false)」這行程式碼出現的位置:

MyRazorWeb\Pages\Books\Edit.cshtml

@page
@model MyRazorWeb.Pages.Books.EditModel
@{
    ViewData["Title"] = "Edit";
}

<h1> Book Edit </h1>
<hr />
<div class = "row">
    <div class = "col-md-8">
        <form method = "post">
            <div asp-validation-summary = "All" class = "text-danger"> </div>
            <input type = "hidden" asp-for = "Book.Id" />
            <div class = "form-group">
                <label asp-for = "Book.Title" class = "control-label"> </label>
                <input asp-for = "Book.Title" class = "form-control" />
                <span asp-validation-for = "Book.Title" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.Price" class = "control-label"> </label>
                <input asp-for = "Book.Price" class = "form-control" />
                <span asp-validation-for = "Book.Price" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.PublishDate" class = "control-label"> </label>
                <input asp-for = "Book.PublishDate" class = "form-control" />
                <span asp-validation-for = "Book.PublishDate" class = "text-danger"> </span>
            </div>
            <div class = "form-group form-check">
                <label class = "form-check-label">
                    <input class = "form-check-input" asp-for = "Book.InStock" /> @Html.DisplayNameFor( model => model.Book.InStock )
                </label>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.Description" class = "control-label"> </label>
                <input asp-for = "Book.Description" class = "form-control" />
                <span asp-validation-for = "Book.Description" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <label asp-for = "Book.Category" class = "control-label"> </label>
                <select asp-for = "Book.Category" class = "form-control"
                        asp-items = "Html.GetEnumSelectList<MyModels.Category>()">
                    <option value = ""> Please Select </option>
                </select>
                <span asp-validation-for = "Book.Category" class = "text-danger"> </span>
            </div>
            <div class = "form-group">
                <input type = "submit" value = "Save" class = "btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page = "./List"> Back to List </a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync( "_ValidationScriptsPartial" );}
}

 

除了叫用「Html.RenderPartialAsync」方法插入「_ValidationScriptsPartial」檔案的程式碼之外,你也可以改用partial標記協助程式(Tag Helper)來插入「_ValidationScriptsPartial」,參可以下範例程式碼:

@section Scripts {

   <partial name="_ValidationScriptsPartial" />

}

選取Visual Studio開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。

在Visual Studio開發工具,按CTRL+F5執行網站「Books\List」Razor Page,執行結果參考如下,現在「Edit」Razor Page便可以使用用戶端驗證了,若使用Chrome瀏覽器除錯工具來測試資料編修作業,當你想要按「Save」按鈕將資料儲存時,用戶端驗證就會生效,從Chrome瀏覽器除錯工具「Network」功能攔不到任何用戶端與伺服端之間有互動,這就代表驗證的動作是發生在用戶端瀏覽器的電腦中,請參考下圖所示:

clip_image016

圖 8:用戶端驗證。

那麼用戶端驗證是如何運作的呢? 使用從Chrome瀏覽器除錯工具「Elements」功能來檢視目前的「Edit」Razor Page,你可以看到許多「data-」開頭的HTML Attribute,加上許多應用在資料驗證的資訊。其中「data-val="true"」啟用用戶端驗證;「data-val-maxlength="長度不可超過 圖書名稱"」設定資料長度超過時要顯示的驗證錯誤訊息;依此類推「data-val-required="圖書名稱不可為空白"」則是資料未輸入時要顯示的錯誤訊息。這些資訊將提供給「jquery-validation-unobtrusive」程式庫來進行用戶端驗證動作。

clip_image018

圖 9:用戶端驗證。

有了用戶端驗證,那麼還需要伺服端驗證嗎? 答案是:「要」。用戶端驗證發生在瀏覽器的電腦上,為了安全性了理由,驗證過程中無法存取伺服端資源,例如想要在新增圖書資料時,檢查這筆資料是否已存在於伺服端的資料庫中,那麼驗證的程式碼就必需撰寫在伺服端。

此外若用戶端瀏覽器關閉用戶端可執行JavaScript的功能,那麼伺服端驗證就是排除有問題資料的唯一選擇了。

Razor Page入門 - 10

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N201022301
出刊日期: 2020/10/14

這篇文章將延續《Razor Page入門 - 9》一文的情境來設計ASP.NET Razor Page網站,到目前為止我們都是針對記憶體中的集合資料來進行操作,在實務上,通常會將資料存放在資料庫,接下來讓我們來介紹如何在ASP.NET Razor Page網站專案整合Entity Framework Core以存取SQL Server資料庫中的資料。

Entity Framework Core(EF Core)

Entity Framework Core(簡稱EF Core)讓開發人員可以使用.NET 物件進行資料庫處理,這種操作方式稱做物件關聯式對應 (object-relational mapper ,O/RM))。EF Core是輕量、可擴充以及跨平台的Entity Framework 版本,目前最新版是Entity Framework Core 3.x版,不包含 Entity Framework 6.x版所有功能。EF Core 使用模型(Model)來進行資料存取,而模型由實體類別(Entity Class) 與「DbContext」物件組成。

安裝Entity Framework Core

資料存取程式碼在多個專案中可以重複使用,因此通常會將它放在一個個別的類別庫專案之中,例如我們的「MyServices」專案。為了要在專案之中使用到Entity Framework Core功能,我們的「MyServices」專案,需要手動安裝 Entity Framework Core相關套件,並選擇適當的資料庫提供者(Data Provider)。在這個範例中,我們將使用「Microsoft.EntityFrameworkCore.SqlServer提供者」,它支援的資料庫引擎為SQL Server 2012 及更新版本,透過此提供者,將資料儲存在微軟的SQL Server Express資料庫。

在「MyServices」專案,使用 Nuget套件管理員下載「Microsoft.EntityFrameworkCore」套件。在「Solution Explorer」視窗選取「MyServices專案,按滑鼠右鍵,從選單選取「Manage NuGet Packages」,請參考下圖所示:

clip_image002

圖 1:開啟Nuget Packages視窗。

在視窗右上方文字方塊中輸入「EntityFrameworkCore」關鍵字搜尋「Microsoft.EntityFrameworkCore」套件,按下右邊的「Install」按鈕進行安裝,請參考下圖所示:

clip_image004

圖 2:安裝「Microsoft.EntityFrameworkCore」套件。

下一個步驟會看到預覽變更的提示視窗,請參考下圖所示:

clip_image006

圖 3:預覽變更。

下一個步驟會看到接受合約畫面,請參考下圖所示:

clip_image008

圖 4:接受合約畫面。

安裝完成後,重複這個步驟,在「MyService」專案安裝「Microsoft.EntityFrameworkCore.SqlServer」套件,內含資料提供者,以支援SQL Server資料庫的存取,請參考下圖所示:

clip_image010

圖 5:安裝「Microsoft.EntityFrameworkCore.SqlServer」套件。

安裝完成後,重複這個步驟,在「MyService」專案安裝「Microsoft.EntityFrameworkCore.Tools」套件,以便讓你使用套件管理員主控台 (Package Manager Console) 命令,請參考下圖所示:

clip_image012

圖 6:安裝「Microsoft.EntityFrameworkCore.Tools」套件。

接著要在「MyRazorWeb」Razor Page網站專案,使用 Nuget套件管理員下載「Microsoft.EntityFrameworkCore.Design」套件。在「Solution Explorer」視窗選取「MyRazorWeb」專案,按滑鼠右鍵,從選單選取「Manage NuGet Packages」,安裝「Microsoft.EntityFrameworkCore.Design」套件,請參考下圖所示:

clip_image014

圖 7:安裝「Microsoft.EntityFrameworkCore.Design」套件。

設計Entity Framework Core Context類別

Entity Framework Core Context是一個很重要的類別,負責與資料庫溝通,透過它可以建立資料庫的連線與異動資料庫資料,此類別需繼承Entity Framework Core的 DbContext類別。你可以在Context類別中定義「DbSet<T>」型別的屬性對應到資料庫資料表 (Table)。

回到「MyServices」專案,加入一個「BookContext」類別。從「Solution Explorer」視窗 -「MyServices」專案上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」,選取「Code」分類下的「Class」項目,然後在下方將「Name」設定為「BookContext」,最後按下「Add」按鈕,請參考下圖所示:

clip_image016

圖 8:加入一個「BookContext」類別。

修改「BookContext.cs 」檔案程式碼,在檔案上方引用「Microsoft.EntityFrameworkCore」、「MyModels」命名空間,修改「BookContext」類別程式碼,使其繼承自「DbContext」類別,並在類別中定義一個名為「Books」,型別為「DbSet<Book>」的屬性,並且使用相依性插入(Depenency Injection)在建構函式中插入服務,以取得「DbContextOptions<BookContext>」,此物件將包含從組態檔案(appsettings.json)取得的資料庫連線字相關資訊:

  • BookContext.cs

using Microsoft.EntityFrameworkCore;
using;
using System;
using System.Collections.Generic;
using System.Text;
namespace MyServices {
  public class BookContext : DbContext {
    public BookContext( DbContextOptions<BookContext> options ) : base( options ) {
    }
    public DbSet<Book> Books { get; set; }
  }
}

 

使用連接字串連接到資料庫

要在ASP.NET Core Razor Page網站建立存取資料的網頁時,資料庫的連線資訊利用字串來存放至「appsettings.json」中的「connectionStrings」區段內,以便連線時參考與方便修改。連接字串是使用「;」組合而成的字串,用來儲存連線至資料庫的資訊,例如:伺服器所在位置(Server)、伺服器登入方式、帳號、密碼與資料庫名稱等;每一組資訊都利用「Key = Value」的方式來描述,而各組資訊之間則使用「;」來區隔。

修改「MyRazorWeb」專案根目錄下的「appsettings.json」檔案,設定連接字串的資料庫伺服器為「.\sqlexpress」;資料庫為「BookDb」,並使用Windows驗證連接到資料庫:

  • MyRazorWeb\appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.\\sqlexpress;Database=BookDb;Trusted_Connection=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

 

在Startup類別註冊DbContext

修改「MyRazorWeb」專案中的「Startup.cs」檔案,在「ConfigureServices」方法「services.Configure<RouteOptions>」這行程式碼上方,加入程式,叫用「IServiceCollection」的「AddDbContextPool」方法,註冊「BookContext」,並使用「Configuration.GetConnectionString」方法讀取「appsettings.json」檔案中的連接字串,以連結到SQL Server Express伺服器的「BookDb」資料庫。同時註解「services.AddSingleton」這行程式碼,目前目前的「Startup.cs」檔案內容如下所示:

  • MyRazorWeb\Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyServices;

namespace MyRazorWeb {
  public class Startup {
    public Startup( IConfiguration configuration ) {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices( IServiceCollection services ) {
      //services.AddSingleton<IBookRepository , BookRepository>();
      services.AddDbContextPool<BookContext>( options => options.UseSqlServer( Configuration.GetConnectionString( "DefaultConnection" ) ) );

      services.Configure<RouteOptions>( options => {
        options.LowercaseUrls = true;
        options.LowercaseQueryStrings = true;
        options.AppendTrailingSlash = true;
      } );
      services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      else {
        app.UseExceptionHandler( "/Error" );
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
      }

      app.UseHttpsRedirection();
      app.UseStaticFiles();

      app.UseRouting();

      app.UseAuthorization();

      app.UseEndpoints( endpoints => {
        endpoints.MapRazorPages();
      } );
    }
  }
}

 

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。

 

使用EF移轉 (EF Migration) 建立資料庫

EF移轉 (EF Migration) 用於更新資料庫結構(Schema),不必重建新資料庫。當資料模型變動時,移轉會更新資料庫結構,保留既有資料。接下來我們將使用使用移轉 (EF Migration) 建立資料庫。

開啟「Package Manager Console」視窗,從Visual Studio 2019開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,設定右方的「Default Project」為「MyServices」,然後在命令提示字元中輸入「add-migration」指令,取一個名稱:

add-migration initial

請參考下圖所示:

clip_image018

圖 9:使用移轉 (EF Migration)。

執行結果參考下圖所示:

clip_image020

圖 10:執行EF移轉 (EF Migration) 命令。

接著Visual Studio會在專案中建立一個「Migrations」資料夾,裏頭包含多個C#檔案,請參考下圖所示:

clip_image022

圖 11:EF移轉 (EF Migration) 產生的程式碼。

其中的「BookContextModelSnapshot.cs」檔案包含當下模型的快照程式,請參考以下程式碼:

  • BookContextModelSnapshot.cs

// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MyServices;

namespace MyServices.Migrations {
  [DbContext( typeof( BookContext ) )]
  partial class BookContextModelSnapshot : ModelSnapshot {
    protected override void BuildModel( ModelBuilder modelBuilder ) {
#pragma warning disable 612, 618
      modelBuilder
          .HasAnnotation( "ProductVersion", "3.1.5" )
          .HasAnnotation( "Relational:MaxIdentifierLength", 128 )
          .HasAnnotation( "SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn );

      modelBuilder.Entity( "MyModels.Book", b => {
        b.Property<int>( "Id" )
            .ValueGeneratedOnAdd()
            .HasColumnType( "int" )
            .HasAnnotation( "SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn );

        b.Property<int?>( "Category" )
            .HasColumnType( "int" );

        b.Property<string>( "Description" )
            .HasColumnType( "nvarchar(50)" )
            .HasMaxLength( 50 );

        b.Property<bool>( "InStock" )
            .HasColumnType( "bit" );

        b.Property<int>( "Price" )
            .HasColumnType( "int" );

        b.Property<DateTime>( "PublishDate" )
            .HasColumnType( "datetime2" );

        b.Property<string>( "Title" )
            .HasColumnType( "nvarchar(max)" );

        b.HasKey( "Id" );

        b.ToTable( "Books" );
      } );
#pragma warning restore 612, 618
    }
  }
}

 

「XXX_initial.cs」檔案則包含變更資料庫結構的程式碼:

  • 20200708060829_initial.cs

using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace MyServices.Migrations {
  public partial class initial : Migration {
    protected override void Up( MigrationBuilder migrationBuilder ) {
      migrationBuilder.CreateTable(
          name: "Books",
          columns: table => new {
            Id = table.Column<int>( nullable: false )
                  .Annotation( "SqlServer:Identity", "1, 1" ),
            Title = table.Column<string>( nullable: true ),
            Price = table.Column<int>( nullable: false ),
            PublishDate = table.Column<DateTime>( nullable: false ),
            InStock = table.Column<bool>( nullable: false ),
            Description = table.Column<string>( maxLength: 50, nullable: true ),
            Category = table.Column<int>( nullable: true )
          },
          constraints: table => {
            table.PrimaryKey( "PK_Books", x => x.Id );
          } );
    }

    protected override void Down( MigrationBuilder migrationBuilder ) {
      migrationBuilder.DropTable(
          name: "Books" );
    }
  }
}

 

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。

開啟「Package Manager Console」視窗,從Visual Studio 2019開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,設定「Default Project」為「MyServices」,然後在命令提示字元中輸入「update-database」指令,以更新資料庫:

update-database

得到的執行結果如下圖所示:

clip_image024

圖 12:使用EF移轉更新資料庫。

檢視資料庫結構描述資訊

開啟Visual Studio開發工具「Server Explorer」視窗,在「Data Connections」項目上按滑鼠右鍵,從快捷選單之中選取「Add Connection」項目,請參考下圖所示:

clip_image026

圖 13:建立資料庫連線。

若出現「Choose Data Source」視窗,則選擇「Microsoft SQL Server」,然後按「Continue」按鈕(這個視窗只有在第一次使用時會自動出現),請參考下圖所示:

clip_image028

圖 14:選擇資料來源。

在「Add Connection」視窗中,設以下屬性,請參考下圖所示:

  • l 資料來源 (Data Source) :Microsoft SQL Server (SqlClient)。
  • l Server name欄位:輸入「.\SQLExpress」。
  • l Authentication:選取「Windows Authentication」。
  • l Select or enter a database name欄位:選擇「BookDb」資料庫。

clip_image030

圖 15:設定連線資訊。

檢視新建立的資料表與欄位資訊,開啟Visual Studio開發工具「Server Explorer」視窗,展開「BookDb」項目,便可看到產生的「Books」資料表,請參考下圖所示:

clip_image032

圖 16:檢視新建立的資料表。

檢視資料表結構,開啟Visual Studio開發工具「Server Explorer」視窗,在「BookDb」-「Books」項目上按滑鼠右鍵,從快捷選單之中選取「Open Table Definition」項目,請參考下圖所示:

clip_image034

圖 17:檢視資料表結構。

資料表結構請參考下圖所示:

clip_image036

圖 18:資料表結構。

設計資料存取程式碼

在「MyServices」專案,加入一個「DbBookRepository」類別。從「Solution Explorer」視窗 -「MyServices」專案上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」,選取「Code」分類下的「Class」項目,然後在下方將「Name」設定為「DbBookRepository」,最後按下「Add」按鈕,請參考下圖所示:

clip_image038

圖 19:加入一個「DbBookRepository」類別。

在「DbBookRepository」類別加入以下程式碼,實作「IBookRepository」介面中所有的方法:

  • DbBookRepository.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using MyModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyServices {
  public class DbBookRepository : IBookRepository {
    private readonly BookContext context;
    public DbBookRepository( BookContext context ) {
      this.context = context;
    }
    public Book Create( Book newBook ) {
      this.context.Books.Add( newBook );
      this.context.SaveChanges();
      return newBook;
    }

    public Book Delete( int id ) {
      Book b = context.Books.Find( id );
      if ( b != null ) {
        context.Books.Remove( b );
        context.SaveChanges();
      }
      return b;
    }

    public IEnumerable<Book> GetAllBooks( ) {
      return context.Books;
    }

    public Book GetBook( int id ) {
      return context.Books.FirstOrDefault( b => b.Id == id );
    }

    public IEnumerable<Book> GetBookByKeyword( string keyword ) {
      return context
              .Books
              .Where( b => b.Title.Contains( keyword ) )
              .ToList();
    }

    public IEnumerable<CategoryCount> GetBookCountByCategory( ) {
      return context
              .Books
              .GroupBy( b => b.Category )
              .Select( g => new CategoryCount() {
                Category = g.Key.Value,
                Count = g.Count()
              } ).ToList();
    }

    public Book Update( Book editBook ) {
      EntityEntry<Book> b = context.Books.Attach( editBook );
      b.State = EntityState.Modified;
      context.SaveChanges();
      return editBook;
    }
  }
}

 

修改「MyRazorWeb」專案中的「Startup.cs」檔案,在「ConfigureServices」方法「services.Configure<RouteOptions>」這行程式碼上方,加入程式,叫用「IServiceCollection」的「AddScoped」方法,註冊「DbBookRepository」,目前的「Startup.cs」檔案內容如下所示:

  • MyRazorWeb\Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyServices;

namespace MyRazorWeb {
  public class Startup {
    public Startup( IConfiguration configuration ) {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices( IServiceCollection services ) {
      //services.AddSingleton<IBookRepository , BookRepository>();
      services.AddScoped<IBookRepository, DbBookRepository>();
      services.AddDbContextPool<BookContext>( options => options.UseSqlServer( Configuration.GetConnectionString( "DefaultConnection" ) ) );

      services.Configure<RouteOptions>( options => {
        options.LowercaseUrls = true;
        options.LowercaseQueryStrings = true;
        options.AppendTrailingSlash = true;
      } );
      services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure( IApplicationBuilder app , IWebHostEnvironment env ) {
      if ( env.IsDevelopment() ) {
        app.UseDeveloperExceptionPage();
      }
      else {
        app.UseExceptionHandler( "/Error" );
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
      }

      app.UseHttpsRedirection();
      app.UseStaticFiles();

      app.UseRouting();

      app.UseAuthorization();

      app.UseEndpoints( endpoints => {
        endpoints.MapRazorPages();
      } );
    }
  }
}

 

選取Visual Studio 開發工具「Build」-「Build Solution」項目編譯目前的專案,確認程式碼能正確編譯。在Visual Studio開發工具,按CTRL+F5執行網站,點選「Books」連結,執行結果參考如下圖所示,一開始資料庫資料表中沒有資料,圖書清單中沒有任何資料:

clip_image040

圖 20:圖書清單。

點選「Create」連結,切換到新增畫面,在表單欄位中輸入要新增的圖書資訊,新增幾筆資料,請參考下圖所示:

clip_image042

圖 21:新增資料。

資料新增完成後將回到「List」頁面,顯示最新圖書清單資料,從中可以看到圖書的最新流水號碼,請參考下圖所示:

clip_image044

圖 22:顯示新增後的資料。

檢視資料表資料

檢視資料表資料,開啟Visual Studio開發工具「Server Explorer」視窗,在「BookDb」-「Books」項目上按滑鼠右鍵,從快捷選單之中選取「Show Table Data」項目,請參考下圖所示:

clip_image046

圖 23:「Show Table Data」項目。

你將可看到,新增的資料出現在資料表之中,請參考下圖所示:

clip_image048

圖 24:顯示資料表資料。

Viewing all 73 articles
Browse latest View live