---
# System prepended metadata

title: '[112]天方科技 ASP.net core 教育訓練 1120330(Linq語法測試、Nullable Reference Types)'

---

# [112]天方科技 ASP.net core 教育訓練 1120330(Linq語法測試、Nullable Reference Types)
## Linq語法測試

### Where
Linq語法可以拆解再組合
```csharp=
        [HttpGet("year/{year}")]
        public ActionResult<IEnumerable<s30_student>> GetStudentByYear(int year, string name)
        {
            var linq = from s in _context.s30_student
                       where s.std_year == year
                       && (name.Length > 0 && s.std_name.StartsWith(name))
                       select s;
            return linq.ToList();
        }

//↓    可以將Where語法另外下條件
[HttpGet("year/{year}")]
        public ActionResult<IEnumerable<s30_student>> GetStudentByYear(int year, string name)
        {
            var linq = from s in _context.s30_student
                       where s.std_year == year
                       //&& (name.Length > 0 && s.std_name.StartsWith(name))
                       select s;

            if(name != null && name.Length > 0)
            {
                linq = linq.Where(s => s.std_name.StartsWith(name));
                //linq = linq.Where(s => EF.Functions.Like(s.std_name, $"{name}%"));  //效能一樣，但是會局限資料提供者對like萬用字元的使用
            }

            return linq.ToList();
        }
```

參考：
[**where 子句 (C# 參考)**](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/where-clause)

### 三元運算子
條件運算子也稱為三元條件運算子 ?: ，會評估布林運算式，並根據布林運算式評估為`true`或`false`，傳回兩個運算式之一的結果
`condition ? consequent : alternative`
一樣使用`Where`語法過濾資料，但是將程式碼簡化成一行，增加可讀性
```csharp=
[HttpGet("year/{year}")]
        public ActionResult<IEnumerable<s30_student>> GetStudentByYear(int year, string? name, string? gender)    //string? name才能忽略name參數
        {
            var linq = from s in _context.s30_student
                       where s.std_year == year
                       && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true)  //使用三元運算子，Where如果回傳true，會略過這一行
                       && (gender != null && gender.Length == 1 ? s.std_sex == gender : true)
                       select s;         

            return linq.ToList();
        }
```
參考：
[**？： 運算子 - 三元條件運算子**](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/operators/conditional-operator)


### 匿名型別
```csharp=
[HttpGet("year/{year}")]
        public ActionResult<IEnumerable<s30_student>> GetStudentByYear(int year, string? name, string? gender)    //string? name才能忽略name參數
        {
            var linq = from s in _context.s30_student
                       where s.std_year == year
                       && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true)  //使用三元運算子，Where如果回傳true，會略過這一行
                       && (gender != null && gender.Length == 1 ? s.std_sex == gender : true)
                       select s;         

            return linq.ToList();
        }
        
        //↓    使用匿名型別
        
        [HttpGet("year/{year}")]
        public IActionResult GetStudentByYear(int year, string? name, string? gender)  //使用匿名型別就無法先指定回傳型別，改用IActionResult
        {
            var linq = from s in _context.s30_student
                       where s.std_year == year
                       && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true) 
                       && (gender != null && gender.Length == 1 ? s.std_sex == gender : true)
                       select new { s.std_key, s.std_id, s.std_name };    //new創建一個沒有名稱的匿名型別

            return Ok(linq.ToList());    //Ok()會回報狀態碼200，ActionResult 會自動幫你將資料包在Ok()，IActionResult要自己手動包
        }
```
### `select s`取得所有欄位
![](https://i.imgur.com/Fi92CFr.png)

### `select new { s.std_key, s.std_id, s.std_name };`只取需要的欄位
![](https://i.imgur.com/fW6A3e4.png)

參考：
[**ActionResult 與 IActionResult**](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/action-return-types?view=aspnetcore-7.0#actionresult-vs-iactionresult)
[**範例 - 查詢語法**](https://learn.microsoft.com/zh-tw/dotnet/csharp/linq/write-linq-queries#example---query-syntax)

### 內插字串
需要給組合的字串一個欄位名稱，其他欄位也可以設定欄位名稱，compiler後的json格式會將欄位名稱都轉為小寫
`select new { Key = s.std_key, Id = s.std_id, Fullname = $"{s.std_name}/{s.std_name_eng}" };`
![](https://i.imgur.com/ul986JD.png)



### Orderby

```csharp=
 [HttpGet("year/{year}")]
        public IActionResult GetStudentByYear(int year, string? name, string? gender)
        {
            var linq = from s in _context.s30_student
                       where s.std_year == year
                       && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true) 
                       && (gender != null && gender.Length == 1 ? s.std_sex == gender : true)
                       orderby s.std_name_eng, s.std_name descending    //預設為ascending(小→大)排序，可以在欄位後設定排序規則
                       select new { Key = s.std_key, Id = s.std_id, Fullname = $"{s.std_name}/{s.std_name_eng}" };

            return Ok(linq.ToList());
        }
```
參考：
[**orderby 子句 (C# 參考)**](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/orderby-clause)

### Group
可以使用`group`子句結束查詢運算式
```csharp=
        [HttpGet("year/{year}")]
        public IActionResult Ge tStudentByYear(int year, string? name, string? gender)
        {
            var linq = from s in _context.s30_student
                       group s by s.cls_id;    //也可以在這裡做運算，例 s.std_year/100
            return Ok(linq.ToList());
        }
```
使用`into`內容關鍵字來指定暫時識別碼，暫時`識別碼.Key`是先前`group by`時所設定的欄位，`cls_id、Student`是自訂欄位名稱 ，如果要看到暫時識別碼的明細資料，必須先`ToList()`
```csharp=
        [HttpGet("year/{year}")]
        public IActionResult GetStudentByYear(int year, string? name, string? gender)
        {
            var linq = from s in _context.s30_student
                       group s by s.cls_id into g    //將group後的資料into暫時識別碼
                       select new {cls_id = g.Key, Student = g.ToList() };    
            return Ok(linq.ToList());    //g.ToList()是取得明細資料，linq.ToList()是真正連線資料庫
        }
```
![](https://i.imgur.com/oMU2zft.png)


暫時識別碼有不少可以使用的函數與屬性
`select new { cls_id = g.Key, Student = g.ToList(), sum = g.Sum(s => s.std_year), max = g.Max(s => s.std_name) };`
![](https://i.imgur.com/MZ7aQWh.png)

`group`後也可以使用`where`語法，同SQL語法的`having`
```csharp=
[HttpGet("year/{year}")]
public IActionResult GetStudentByYear(int year, string? name, string? gender)
{
    var linq = from s in _context.s30_student
               where s.std_year == year
               && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true)
               && (gender != null && gender.Length == 1 ? s.std_sex == gender : true)
               group s by s.cls_id into g
               where g.Count() > 0
               select new { cls_id = g.Key, Student = g.ToList(), sum = g.Sum(s => s.std_year), max = g.Max(s => s.std_name) };
    return Ok(linq.ToList());
}
```
```sql=
SELECT [s].[cls_id], COALESCE(SUM([s].[std_year]), 0) AS [c], MAX([s].[std_name]) AS [c0]
          FROM [s30_student] AS [s]
          WHERE [s].[std_year] = @__year_0 AND (@__name_1 = N'' OR (([s].[std_name] IS NOT NULL) AND LEFT([s].[std_name], LEN(@__name_1_1)) = @__name_1_1)) AND [s].[std_sex] = @__gender_2
          GROUP BY [s].[cls_id]
          HAVING COUNT(*) > 0
```
![](https://i.imgur.com/q54xrKZ.png)




參考：
[**group 子句 (C# 參考)**](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/group-clause)
### Join
`Join`子句只能使用對等式，所以在`on`後只能使用`equals`而不是常用的`==`，同時在`from a join b`的條件上，前後順序不能變動，一定要是`a.欄位 equals b.欄位`
```csharp=
        [HttpGet("join")]
        public IActionResult GetJoin()
        {
            var linq = from a in _context.s30_student
                       join b in _context.s90_class on a.cls_id equals b.cls_id
                       select new { a.std_name, a.std_year, b.cls_name_abr};
            
            return Ok(linq.ToList());
        }
```
一般情況轉換出來都是使用`inner join`
```sql=
    SELECT [s].[std_name], [s].[std_year], [s0].[cls_name_abr]
    FROM [s30_student] AS [s]
    INNER JOIN [s90_class] AS [s0] ON [s].[cls_id] = [s0].[cls_id]
```
使用兩個`From`，會使用`Close join`
```csharp=
        [HttpGet("join")]
        public IActionResult GetJoin()
        {              
            var linq = from a in _context.s30_student
                       from b in _context.s90_class
                       where a.cls_id.CompareTo(b.cls_id) > 0 //字串比較，使用CompareTo()
                       join c in _context.s90_units on b.unt_id equals c.unt_id
                       join d in _context.s90_cmparea on c.unt_cmparea equals d.cmp_area
                       select new { a.std_name, a.std_year, b.cls_name_abr, b.cls_id, c.unt_name_abr, d.cmp_name_abr };

            return Ok(linq.ToList());
        }
```

```sql=
      SELECT [s].[std_name], [s].[std_year], [s0].[cls_name_abr], [s0].[cls_id], [s1].[unt_name_abr], [s2].[cmp_name_abr]
      FROM [s30_student] AS [s]
      CROSS JOIN [s90_class] AS [s0]
      INNER JOIN [s90_units] AS [s1] ON [s0].[unt_id] = [s1].[unt_id]
      INNER JOIN [s90_cmparea] AS [s2] ON [s1].[unt_cmparea] = [s2].[cmp_area]
      WHERE [s].[cls_id] > [s0].[cls_id]
```

參考：
[**join 子句 (C# 參考)**](https://learn.microsoft.com/zh-tw/dotnet/csharp/language-reference/keywords/join-clause)

## Nullable Reference Types
### C# 8 的 Nullable Reference Types 意味著往後所有的參考型別預設都是不可為 null；對於可為 null 的參考型別變數，寫法跟可為 null 的實質型別一樣，宣告時必須在型別後面加上 "?" 字元。請看以下範例：
```csharp=
int? num = null;         // 可為 null（實值型別）
string? userName = null; // 可為 null（參考型別）
string password = null;  // 編譯警告: 不可為 null!
Employee? emp = null;    // 可為 null（參考型別）
User user = null;        // 編譯警告: 不可為 null!
```


### 為什麼需要 Nullable Reference Types？

在 C# 8 之前，由於參考型別的變數值有兩種可能：null 或者不是 null，編譯器在這裡幫不上忙，所以在處理參考型別的變數時，我們常常會加上一些「防護罩」，也就是預先檢查變數是否為 null，例如：
```csharp=
static int Length(string text)
{
    return text == null? 0 : text.Length;
}
```
就上例來說，我們在寫`Length`函式時，根本無法確定傳入的參數`text`究竟有沒有值。如果不先檢查變數是否為`null`就貿然使用它的屬性或方法，那麼當程式執行時，只要呼叫端傳入一個`null`，就會發生`NullReferenceException`類型的錯誤

### 如果要讓參數`text`接受`null`，便需要用到 C# 8 的新語法，也就是在宣告變數的時候，型別的後面加上一個問號。如下所示：
```csharp=
static int Length(string? text) // text 可以傳入 null。
{
    return text == null? 0 : text.Length;
}
```
C# 8 編譯器會把上述程式碼編譯成類似底下這樣：
```csharp=
public static int Length([Nullable] string s)
{
    return (s == null) ? 0 : s.Length;
}
```
參考：
[**初探 C# 8 的 Nullable Reference Types**](https://www.huanlintalk.com/2018/02/c-8-nullable-reference-types.html)

## ApiController 
### 屬性 `ApiController` 可以套用至控制器類別，以啟用下列意見化 API 特定行為：

屬性路由需求
`HTTP 400` 自動回應
繫結來源參數推斷
多部分/表單資料要求推斷
錯誤狀態碼的問題詳細資料

參考：
[**ApiController 屬性**](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/?view=aspnetcore-3.1#apicontroller-attribute-2)

## 字串處理
參考：
[**彙整從 C# 1.0 到 C# 11.0 的字串格式變化**](https://blog.miniasp.com/post/2023/01/10/CSharp-String-Literals-Syntax-Collection)