# [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)