# [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`取得所有欄位

### `select new { s.std_key, s.std_id, s.std_name };`只取需要的欄位

參考:
[**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}" };`

### 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()是真正連線資料庫
}
```

暫時識別碼有不少可以使用的函數與屬性
`select new { cls_id = g.Key, Student = g.ToList(), sum = g.Sum(s => s.std_year), max = g.Max(s => s.std_name) };`

`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
```

參考:
[**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)