[Day22] C# MVC API版本控制 - C#&AspNetCore

在上回中我們介紹了 [Day21] C# MVC RESTful API (下) 實作RESTful API - C#&AspNetCore ,實作了RESTful API。

而這回將要進行API版本控制!
能處理前端(React/Vue)或APP(iOS/Android)不能馬上更新的最佳手段,有錯可以馬上先還原上一版本、達到向下相容。

API規格

可修改apiVersion調用不同版本API,如:v1.0、v2.0

方法 網址 描述 要求文本 回應文本
GET {apiVersion}/api/todo 取得所有待辦事項 待辦事項陣列
GET {apiVersion}/api/todo/{id} 依識別碼取得待辦事項 待辦事項
POST {apiVersion}/api/todo 新增待辦事項 待辦事項 待辦事項
PUT {apiVersion}/api/todo/{id} 更新現有的待辦事項 待辦事項 待辦事項
DELETE {apiVersion}/api/todo/{id} 刪除待辦事項 待辦事項
GET {apiVersion}/api/version 取得API版本 API版本

實作API版控

先列出我們要改的檔案隻數:

1.安裝Microsoft.AspNetCore.Mvc.Versioning

2.再來更新Startup.cs,服務增加一行啟用API功能services.AddApiVersioning()

1
2
3
4
5
6
7
8
9
10
11
12
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 資料庫配置
var connection = @"Server=.\SQLExpress;Database=TodoDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<TodoDBContext>(options => options.UseSqlServer(connection));

// 啟用API版控功能
services.AddApiVersioning();

}

3.以資料夾分版本層,就是把原本的Controller複製至版本資料夾

如1.0版:
~/Controllers/V1/VersionController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace TodoAPI.Controllers.V1
{
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v1" };
}
}
}

如2.0版:

~/Controllers/V2/VersionController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace TodoAPI.Controllers.V2
{
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v2" };
}
}
}

只要在每個Controller修改以下的地方即可

1
namespace TodoAPI.Controllers.V1 

改成

1
namespace TodoAPI.Controllers.V2

以及

1
[ApiVersion("1.0")]

改成

1
[ApiVersion("2.0")]

TodoController.cs也是一樣喔,就不示範了。

4.嘗試看看

總結

原本很難的事情,竟然可以如此簡單解決!感動!
再來就是考驗DB規劃了,因為如果要向下兼容變成只能增不能刪。
但通常我們能在上與下版間先除錯,所以基本上不會有太大問題。

而LINQ強型別好處就來了,因為是透過Model存取的關係,所以能夠有效避免規格錯誤。

而網址用v1.0、v2.0…原因在Get時瀏覽器只能用網址,所以是最方便的方式。
當然也能夠改放在header、或用?api=1.0的做法。

最後附上完整程式碼:
https://github.com/yuhsiang237/ASP.NET-Core-RESTfulAPI-API-Version-Control

再來就是添加Swagger:
[Day23] C# MVC Web API版本增加Swagger - C#&AspNetCore

額外補充:
至於Controller多版本那Model呢?
因為向下兼容,可能會回傳不同版的Model
這時就可以使用繼承跟覆寫!
如1.1繼承1.0:ModelV1_1:ModelV1_0
之後添加屬性跟覆寫方法
而主要就是避免資料庫異動太大,就可以避免break change造成不相容。

參考資料
https://dotblogs.com.tw/rainmaker/2017/03/12/130759

[Day21] C# MVC RESTful API (下) 實作RESTful API - C#&AspNetCore

在上回中我們介紹了 [Day20] C# MVC RESTful API (中) 建立API專案 - C#&AspNetCore ,我們在原本的MVC專案中加入了WEB API專案。

而這回將會撰寫RESTful API!
本文會以:https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/first-web-api?view=aspnetcore-3.1&tabs=visual-studio 提供的範例作為實作依據。

RESTful API規格

方法 網址 描述 要求文本 回應文本
GET /api/todo 取得所有待辦事項 待辦事項陣列
GET /api/todo/{id} 依識別碼取得待辦事項 待辦事項
POST /api/todo 新增待辦事項 待辦事項 待辦事項
PUT /api/todo/{id} 更新現有的待辦事項 待辦事項 待辦事項
DELETE /api/todo/{id} 刪除待辦事項 待辦事項

實作RESTful API

先安裝基本資料庫套件
1.Microsoft.EntityFrameworkCore

2.Microsoft.EntityFrameworkCore.SqlServer

3.Microsoft.EntityFrameworkCore.Tools

4.使用DB First

Scaffold-DbContext指令,來還原TodoDB資料庫的Models

1
Scaffold-DbContext "Server=.\SQLExpress;Database=TodoDB;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

執行畫面:

執行後:

5.Startup.cs增加資料庫配置

1
2
3
4
5
6
7
8
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// 資料庫配置
var connection = @"Server=.\SQLExpress;Database=TodoDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<TodoDBContext>(options => options.UseSqlServer(connection));
}

6.撰寫RESTful API

~/Controllers/TodoController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoAPI.Models;

namespace TodoAPI.Controllers
{

[Route("api/[controller]")]
[ApiController]
public class TodoController : ControllerBase
{
private readonly TodoDBContext _context;
public TodoController(TodoDBContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Todo>>> GetTodos()
{
return await _context.Todos.Where(x=>x.IsDeleted != 1).ToListAsync();
}

[HttpGet("{id}")]
public async Task<ActionResult<Todo>> GetTodo(int id)
{
var obj = await _context.Todos.FindAsync(id);

if (obj.IsDeleted == 1)
{
return NotFound();
}

return obj;
}

[HttpPut("{id}")]
public async Task<ActionResult<Todo>> PutTodo(int id, Todo newObj)
{
if (id != newObj.Id)
{
return BadRequest();
}

var obj = await _context.Todos.FindAsync(id);

if(obj.IsDeleted == 1)
{
return NotFound();
}

obj.IsComplete = newObj.IsComplete;
obj.Name = newObj.Name;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TodoExists(id))
{
return NotFound();
}
else
{
throw;
}
}

return obj;
}

[HttpPost]
public async Task<ActionResult<Todo>> PostTodo(Todo newObj)
{
var obj = new Todo
{
Name = newObj.Name,
IsComplete = 0,
IsDeleted = 0
};
_context.Todos.Add(obj);
await _context.SaveChangesAsync();

return CreatedAtAction(nameof(GetTodo), new { id = obj.Id }, obj);
}
[HttpDelete("{id}")]
public async Task<ActionResult<Todo>> DeleteTodo(int id)
{
var obj = await _context.Todos.FindAsync(id);
obj.IsDeleted = 1;

if (obj == null)
{
return NotFound();
}
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TodoExists(id))
{
return NotFound();
}
else
{
throw;
}
}

return obj;
}

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

7.用postman測試

專案程式碼:
https://github.com/yuhsiang237/ASP.NET-Core-RESTfulAPI

總結

這樣就完成了RESTful API了!

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/first-web-api?view=aspnetcore-3.1&tabs=visual-studio

[Day20] C# MVC RESTful API (中) 建立API專案 - C#&AspNetCore

在上回中我們介紹了 [Day19] C# MVC RESTful API (上) RESTful 基礎知識 - C#&AspNetCore ,知道了RESTful API的基礎知識。

這回先了解如何在現有的MVC專案中加入API專案吧!

MVC專案加入WEB API專案

1.這是一個原本的MVC專案

2.對Solution點右鍵>New Project

3.選ASP.NET Core Web API

4.建立API專案名稱,這取名TodoAPI

5.版本我選.net core 3.1長期穩定版本

6.這時就會有一個專案TodoAPI加入現有的Solution了。

7.切換運行的專案

8.執行,可出現預設的範例API結果

總結

在這篇中主要是探討如何在一個有MVC專案的狀況下再去加一個Web API服務。
而在下篇我們就要用這範例實作RESTful API。

[Day19] C# MVC RESTful API (上) RESTful 基礎知識 - C#&AspNetCore

在上回中我們介紹了 [Day18] C# MVC專案中撰寫API - C#&AspNetCore ,能在MVC專案中撰寫API了。

而這回,探討Web API,他會開給APP(iOS或Android)、前端網站(React或Vue)這種主從式分離的架構。
為了能開出比較公版的API,就必須先了解RESTful的核心概念。
(這次嘗試以翻原文的方式撰寫,往後都盡可能以原文為主)

RESTful 介紹

Representational state transfer (REST) is a software architectural style that was created to guide the design and development of the architecture for the World Wide Web.REST defines a set of constraints for how the architecture of an Internet-scale distributed hypermedia system, such as the Web, should behave.The REST architectural style emphasises the scalability of interactions between components, uniform interfaces, independent deployment of components, and the creation of a layered architecture to facilitate caching components to reduce user-perceived latency, enforce security, and encapsulate legacy systems.

REST是一種軟體架構的風格,被創立來引導網站設計、開發架構。並且定義了約束架構,像是在網站上的行為。
架構上強調在組件間的可擴展性、統一的接口、獨立部署的組建、創建分層架構以建立快取減少用戶延遲、強化安全性、封裝老舊系統。

The REST architectural style is designed for network-based applications, specifically client-server applications. But more than that, it is designed for Internet-scale usage, so the coupling between the user agent (client) and the origin server must be as lightweight (loose) as possible to facilitate large-scale adoption.

被用來處理網路基礎的應用,具體來說在主從式架構的應用。但不止如此,也被用來作為互聯網的使用而設計,因此客戶端、主機端耦合度盡可能可能輕量級,以使在大規模時容易採用。

耦合度(Coupling):coupling is the degree of interdependence between software modules。Coupling is usually contrasted with cohesion. Low coupling often correlates with high cohesion,
耦合度表示模組間相互依賴的程度。
耦合通常與內聚相關,低耦合通常與高內聚相關。
如下的(a)就是比較好的結構,盡量封裝好,開簡單的接口出來,但現實情況下不可能做到理想。
以我自身經驗,曾維護那種(b)的狀況,要搞懂A還得先去搞懂B在幹嘛,追了8層之多。此外,精簡的程式碼不等於高內聚,有時更多的是忍者程式碼(Ninja Code)

RESTful 6個限制

  1. 主從式架構(Client–server architecture)
    The client-server design pattern enforces the principle of separation of concerns
    主從式設計模式強制關注點分離的原則
    separating the user interface concerns from the data storage concerns. Portability of the user interface is thus improved.
    分離用戶介面與資料儲存問題。用戶介面的可攜性因此被改善。

  2. 無狀態(Statelessness)
    In computing, a stateless protocol is a communications protocol in which no session information is retained by the receiver, usually a server. Relevant session data is sent to the receiver by the client in such a way that every packet of information transferred can be understood in isolation, without context information from previous packets in the session.
    無狀態是種溝通的協定,沒有session被接收者保留,通常接收者是伺服器端。客戶端將資訊送給接收者,每個資訊可以獨立離解,不用藉由理解上下文才能知道訊息。

  3. 快取(Cacheability)
    As on the World Wide Web, clients and intermediaries can cache responses. Responses must, implicitly or explicitly, define themselves as either cacheable or non-cacheable to prevent clients from providing stale or inappropriate data in response to further requests. Well-managed caching partially or completely eliminates some client–server interactions, further improving scalability and performance.
    在網路上客戶、中間人可以快取結果,可自行定義是否該緩存的資料。良好的快取能改善效能。

  1. 分層系統(Layered system)
    A client cannot ordinarily tell whether it is connected directly to the end server or to an intermediary along the way.Adding security as a separate layer enforces security policies.
    客戶端不能判斷是連結到終端還是中介。能增加安全性在特別的層級強制特定的安全政策。

  2. 按需求編碼(Code on demand) 可選
    Servers can temporarily extend or customize the functionality of a client by transferring executable code: for example, compiled components such as Java applets, or client-side scripts such as JavaScript.
    伺服器端可臨時擴充或可執行代碼給客戶端,如:客戶端代碼JavaScript、編譯後元件Java小程式。

  3. 統一接口(Uniform interface)
    The uniform interface constraint is fundamental to the design of any RESTful system
    統一接口約束是restful系統的基礎。
    為以下四點:

    1. 請求中包含資源的 ID(Resource identification in requests)
      求中包含了各種獨立資源的標識,例如,在Web服務中的URI。資源本身和傳送給客戶端的標識是獨立。例如,伺服器可以將自身的資料庫資訊以HTML、XML或者JSON的方式傳送給客戶端,但是這些可能都不是伺服器的內部記錄方式。
    2. 資源通過標識來操作(Resource manipulation through representations)
      當客戶端擁有一個資源的標識,包括附帶的元資料,則它就有足夠的資訊來刪除這個資源。
    3. 訊息的自我描述性(Self-descriptive messages)
      每個訊息包含足夠資訊描述如何處理訊息。
    4. 用超媒體驅動應用狀態(Hypermedia as the engine of application state (HATEOAS)
      同使用者存取Web伺服器的Home頁面相似,當一個 REST 客戶端存取了最初的REST應用的URI之後,REST 客戶端應該可以使用伺服器端提供的連結,動態的發現所有的可用的資源和可執行的操作。

RESTful優點

  • 可更高效利用快取來提高回應速度
  • 通訊本身的無狀態性可以讓不同的伺服器的處理一系列請求中的不同請求,提高伺服器的擴充性
  • 瀏覽器即可作為客戶端,簡化軟體需求
  • 相對於其他疊加在HTTP協定之上的機制,REST的軟體相依性更小
    不需要額外的資源發現機制
  • 在軟體技術演進中的長期的相容性更好

HTTP請求方法在RESTful API中的典型應用

資源 GET PUT POST DELETE
一組資源的URI,比如https://example.com/resources 列出URI,以及該資源組中每個資源的詳細資訊(後者可選)。 使用給定的一組資源替換當前整組資源。 在本組資源中建立/追加一個新的資源。該操作往往返回新資源的URL。 刪除整組資源。
單個資源的URI,比如https://example.com/resources/142 取得指定的資源的詳細資訊,格式可以自選一個合適的網路媒體類型(比如:XML、JSON等) 替換/建立指定的資源。並將其追加到相應的資源組中。 把指定的資源當做一個資源組,並在其下建立/追加一個新的元素,使其隸屬於當前資源。 刪除指定的元素。

RESTful衍伸問題

  1. RESTful的批量處理問題,要一次處理多筆資料
    RESTful給出了處理單筆資料的方式,卻沒多介紹多筆,因此參考以下文件後發現可以用example.com/resources/batch的方式處理
    https://www.npmjs.com/package/restful-api
    https://cloud.tencent.com/developer/article/1575071
  2. RESTful的權限規劃問題,以資源式不好管理
    後台通常以Role Based Access Control (RBAC) 去規劃權限。
    而RESTful是以資源(Resources)為底,這時判斷哪個API具有某權限就必須特別判斷或添加。如沒限制好可能造成資料多給的漏洞。
    如果是我會以敏感資訊為大方向去限制。
  3. RESTful的客戶端邏輯,錯在誰身上
    因為關注點分離,所以難知道客戶端如Android/iOS的程式有問題時是落在API人員身上還是前端身上。
    有點我資料都給你了,你自己處理的感覺。

總結

在這篇中我們主要探討RESTful的核心理論部分,也發現RESTful在現實使用上的問題。
在我的看法上,使用RESTful的原因在於:

  1. API統一的風格設計
  2. 有特定設計模式利於維護性
  3. 能減少開發成本、開發時間

如果沒法達成以上幾點,那額外使用自訂的API也無妨。
比如Google真有做到全RESTful嗎?
稍微查看Google Drive、Gmail就會發現沒有,還是用一些getXXX的API。只有開給其他開發者才用RESTful API。

因此,直覺上比較適合開給外部的其他人而使用。

最後,實作將在下篇開始進行。

參考資料
https://en.wikipedia.org/wiki/Coupling_(computer_programming)#/media/File:CouplingVsCohesion.svg
https://en.wikipedia.org/wiki/Coupling_(computer_programming)
https://zh.wikipedia.org/wiki/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2
https://cloud.tencent.com/developer/article/1497547
https://cloud.tencent.com/developer/article/1575071
https://www.restapitutorial.com/httpstatuscodes.html#google_vignette
https://noob.tw/restful-api/

[Day18] C# MVC專案中撰寫API - C#&AspNetCore

在上回中我們介紹了 [Day17] C# MVC 排序、篩選和分頁實作 - C#&AspNetCore ,能呈現出列表了。

而這回就要講能讓事情比較省事的API。

API

應用程式介面(英語:Application Programming Interface),縮寫為API,是一種計算介面,它定義多個軟體中介之間的互動,以及可以進行的呼叫(call)或請求(request)的種類,如何進行呼叫或發出請求,應使用的資料格式,應遵循的慣例等。

簡單來說就是:程式的接口,可以傳送參數給後端處理的函式,之後會把所需的資料回傳你。

實作API

這回先介紹任意命名的API。

1.在Controller撰寫API,第37~45行

~/Controller/HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using MVC_With_API.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace MVC_With_API.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
return View();
}

public IActionResult Privacy()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

[HttpPost]
public JsonResult getSum(int num)
{
int sum = 0;
for(int i = 0; i < num; i++)
{
sum += i;
}
return Json(new { result = sum });
}
}
}

說明:
如上第37~45行就是一個API了,外界可以用POST方式Request,然後經由裡面的計算去得到一個結果,並回傳Response。
這種隨意風格的API大概僅適合在特定Controller底下使用。

2.再來我們用測試工具PostMan查看結果
https://www.postman.com/downloads/
說明:我們用body傳送了參數num,並設定為10,會從1+2+3…+10=45得到45,可從Response接到值。

3.嘗試在頁面上呼叫

~/Views/Home/Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@{
ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<div id="result"></div>

@section Scripts{
<script type="text/javascript">
$.post("Home/getSum",
{
num: "10",
},
function (data, status) {
$('#result').html(data.result)
});
</script>
}

說明:第95~105行就是呼叫的AJAX了!
並把結果印到html上。

結果:

總結

這種簡易無規範的API比較適合只在Controller底下寫。
如果要開給外界比較推薦用RESTful API。

至於為什麼RESTful呢?
因為各種getPeopleData、updatePeople、getPeopleList…會造成維護上不好掌控,沒有固定格式,不好直覺知道結果。
但如果專案內部有風格統一API,且偏向自用,那其實也不需要RESTful。

下回會嘗試使用RESTful API的方式撰寫。

然後相較於之前Laravel開發的經驗,感覺.net core MVC切路由方式不太直覺@@。

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/web-api/?view=aspnetcore-3.1

[Day17] C# MVC 排序、篩選和分頁實作 - C#&AspNetCore

對於資料庫的操作我們介紹了兩種做法:

而在上方兩篇文中我們都是撈頁面呈現單一資料。
因此這回來談談列表吧!

目標

以下是整體的目標:

  • 可搜尋過濾
  • 可排序
  • 可分頁

基本上有這三種功能,就是一個強大的列表了。
而剛好官方就有提供這方面的實作教學,因此就參考官方作法:
https://docs.microsoft.com/zh-tw/aspnet/core/data/ef-mvc/sort-filter-page?view=aspnetcore-3.1

實作

在此範例使用Entity Framework Core作為示範。

先附上完成後結果:

修改檔案:

  • ~/Commons/PaginatedList.cs:分頁列表類別檔案,協助我們製作分頁。
  • ~/Controllers/OrderController.cs:訂單列表的控制器,負責整張列表的邏輯,如排序、分頁、搜尋。
  • ~/Models/ViewModels/OrderIndexViewModel.cs:用來呈現列表規格的ViewModel。
  • ~/Views/Order/Index.cshtml:列表畫面程式碼。

1.首先先建立列表分頁的類別

~/Commons/PaginatedList.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ListExample
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }

public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);

this.AddRange(items);
}

public bool HasPreviousPage
{
get
{
return (PageIndex > 1);
}
}

public bool HasNextPage
{
get
{
return (PageIndex < TotalPages);
}
}

public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}

說明:這是官方提供的類別,不需要動。

  • CreateAsync:主要讓我們把資料以PaginatedList的方式傳給View。
  • HasPreviousPage:判斷上一頁面是否存在,用來啟動跟關閉上一頁按鈕。
  • HasNextPage:判斷下一頁面,用來啟動跟關閉下一頁按鈕。

2.再來是資料規格,因為我們的資料有使用到兩張資料表(顧客、訂單),因此會需要用到ViewModel。

~/Models/ViewModels/OrderIndexViewModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
using System.ComponentModel.DataAnnotations;
namespace ListExample.Models.ViewModels
{
public class OrderIndexViewModel
{
[Display(Name = "訂單編號")]

public string Number { get; set; }
[Display(Name = "寄送日期")]

public DateTime ShippingDate { get; set; }
[Display(Name = "寄送地址")]

public string ShippingAddress { get; set; }
[Display(Name = "客戶簽收")]

public string CustomerSignature { get; set; }
[Display(Name = "客戶編號")]

public string CustomerNumber { get; set; }
[Display(Name = "總額")]

public decimal Total { get; set; }
[Display(Name = "客戶名稱")]

public string CustomerName { get; set; }
[Display(Name = "客戶電話")]

public string CustomerTel { get; set; }

}
}

3.之後是Controller,負責我們分頁、查詢、排序的主要商業邏輯。
~/Controllers/OrderController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
using ListExample.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Threading.Tasks;
using System.Linq;
using ListExample.Models.ViewModels;

namespace ListExample.Controllers
{
public class OrderController : Controller
{

private readonly OrdersContext _context;

public OrderController(OrdersContext context)
{
_context = context;
}
public async Task<IActionResult> Index(
string sortOrder,
string currentFilterCustomer,
string currentFilterNumber,
string searchStringCustomer,
string searchStringNumber,
int? goToPageNumber,
int pageSize,
int? pageNumber)
{
// 1.搜尋邏輯
var query = from a in _context.Orders
join b in _context.Customers on a.CustomerNumber equals b.Number
into result1
from ab in result1.DefaultIfEmpty()
select new OrderIndexViewModel
{
Number = a.Number,
ShippingAddress = a.ShippingAddress,
ShippingDate = a.ShippingDate,
CustomerSignature = a.CustomerSignature,
Total = a.Total,
CustomerNumber = a.CustomerNumber,
CustomerName = ab.Name,
CustomerTel = ab.Tel
};

// 2.條件過濾
if (searchStringCustomer != null || searchStringNumber != null)
{
pageNumber = 1;
}
else
{
searchStringCustomer = currentFilterCustomer;
searchStringNumber = currentFilterNumber;
}

ViewData["CurrentFilterCustomer"] = searchStringCustomer;
ViewData["CurrentFilterNumber"] = searchStringNumber;

if (!String.IsNullOrEmpty(searchStringCustomer))
{
query = query.Where(s => s.CustomerName.Contains(searchStringCustomer));
}
if (!String.IsNullOrEmpty(searchStringNumber))
{
query = query.Where(s => s.Number.Contains(searchStringNumber));
}

// 3.排序依據
ViewData["CurrentSort"] = sortOrder;

switch (sortOrder)
{
case "1":
query = query.OrderByDescending(s => s.ShippingDate);
break;
case "2":
query = query.OrderBy(s => s.ShippingDate);
break;
case "3":
query = query.OrderByDescending(s => s.Total);
break;
case "4":
query = query.OrderBy(s => s.Total);
break;
default:
query = query.OrderByDescending(s => s.ShippingDate);
break;
}

// 4.前往頁數
if (goToPageNumber != null)
{
pageNumber = goToPageNumber;
}

// 5.每頁筆數
if (pageSize == 0)
{
pageSize = 10;
}
ViewData["pageSize"] = pageSize;

// 6.返回結果
return View(await PaginatedList<OrderIndexViewModel>.CreateAsync(query.AsNoTracking(), pageNumber ?? 1, pageSize));
}

}
}

說明:這範例也是由官方改造而成。

  • Index的參數:
    • sortOrder:排序的參數。
    • currentFilterCustomer:過濾Customer(客戶名稱)搜尋框的參數,暫存需要。
    • currentFilterNumber:過濾Number(訂單編號)搜尋框的參數,暫存需要。
    • searchStringCustomer:過濾Customer(客戶名稱)搜尋框的參數。
    • searchStringNumber:過濾Number(訂單編號)搜尋框的參數。
    • goToPageNumber:跳到某頁的參數。
    • pageSize:每頁筆數。
    • pageNumber:目前在第幾頁面。

主要流程為:建立查詢>過濾>排序>分頁。

  • 第31~45行:建立LINQ搜尋。
  • 第47~68行:用LINQ過濾,因為可能會有很多搜尋參數,在這範例中就有兩個,但要增加N個改法都一樣。算是:KISS (Keep It Simple, Stupid)。
  • 第71~90行:用LINQ排序。
  • 第92~103行:每頁筆數限制、跳至指定頁。
  • 第105行:回傳結果。

而使用ViewData是因為一些字串還需要留在頁面上,如:分頁、搜尋字串、排序。

4.前台畫面呈現

~/Views/Order/Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
@model PaginatedList<ListExample.Models.ViewModels.OrderIndexViewModel>
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
}
<h1>Order</h1>

<form asp-action="Index" class="mt-3" id="form_search">
<div class="row">
<div class="col-sm col-md-6 col-lg-3 col-xl-3">
<label class="custom-label">客戶名稱</label>
<div class="form-group form-row">
<div class="col">
<input id="input_customer" type="text" class="form-control mr-2" name="searchStringCustomer" value="@ViewData["CurrentFilterCustomer"]" />
</div>
</div>
</div>
<div class="col-sm col-md-6 col-lg-3 col-xl-3">
<label class="custom-label">訂單編號</label>
<div class="form-group form-row">
<div class="col">
<input id="input_number" type="text" class="form-control mr-2" name="searchStringNumber" value="@ViewData["CurrentFilterNumber"]" />
</div>
</div>
</div>
</div>
<div class="text-right">
<button type="submit" class="btn btn-secondary mb-2">查詢資料</button>
<button class="btn btn-outline-secondary mb-2" onclick="clearSearch();">清空查詢</button>
</div>
<hr class="mt-0">
<div class="d-flex justify-content-end">
<div class="col-2 px-0">
<div class="form-group">
<select class="form-control" name="sortOrder" onchange="this.form.submit()">
<option value="0" selected="@((string)ViewData["CurrentSort"] == "0")">預設排序</option>
<option value="1" selected="@((string)ViewData["CurrentSort"] == "1")">出貨日期 高→低</option>
<option value="2" selected="@((string)ViewData["CurrentSort"] == "2")">出貨日期 低→高</option>
<option value="3" selected="@((string)ViewData["CurrentSort"] == "3")">訂單總額 高→低</option>
<option value="4" selected="@((string)ViewData["CurrentSort"] == "4")">訂單總額 低→高</option>
</select>
</div>
</div>
</div>


<div class="table-responsive">

<table class="table table-bordered">
<thead>
<tr>
<th>
訂單編號
</th>
<th>
出貨日期
</th>
<th>
寄送地址
</th>
<th>
客戶編號
</th>
<th>
客戶名稱
</th>
<th>
客戶電話
</th>
<th>
客戶簽收人
</th>
<th>
訂單總額
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Number)
</td>

<td>
@Convert.ToDateTime(item.ShippingDate).ToString("yyyy-MM-dd")
</td>
<td>
@Html.DisplayFor(modelItem => item.ShippingAddress)
</td>
<td>
@Html.DisplayFor(modelItem => item.CustomerNumber)
</td>
<td>
@Html.DisplayFor(modelItem => item.CustomerName)
</td>
<td>
@Html.DisplayFor(modelItem => item.CustomerTel)
</td>
<td>
@if (item.CustomerSignature != "" && item.CustomerSignature != null)
{
@Html.DisplayFor(modelItem => item.CustomerSignature)
}
else
{
<span>-</span>
}
</td>
<td>
@Html.DisplayFor(modelItem => item.Total)
</td>
</tr>
}
</tbody>
</table>
</div>
@{
var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<div class="list-pagination mt-3">
<div class="form-inline text-center">
<div class="mx-auto">
每頁
<select class="custom-select" name="pageSize" onchange="changePageSize()">
<option value="5" selected="@((int)ViewData["pageSize"]==5)">5</option>
<option value="10" selected="@((int)ViewData["pageSize"]==10)">10</option>
<option value="30" selected="@((int)ViewData["pageSize"]==30)">30</option>
<option value="50" selected="@((int)ViewData["pageSize"]==50)">50</option>
</select>,第 <span>@(Model.TotalPages < Model.PageIndex ? 0 : Model.PageIndex) / @Model.TotalPages</span> 頁,共 <span>@Model.TotalPages</span> 頁,
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilterNumber="@ViewData["CurrentFilterNumber"]"
asp-route-CurrentFilterCustomer="@ViewData["CurrentFilterCustomer"]"
asp-route-pageSize="@ViewData["PageSize"]"
class="btn btn-outline-secondary btn-sm @prevDisabled">
上一頁
</a>|跳至第
<select id="select_goToPage" class="custom-select" name="goToPageNumber" onchange="goToPage();">
<option>
選擇
</option>
@for (var i = 1; i <= Model.TotalPages; i++)
{
<option value="@i" selected="@(Model.PageIndex == i)">
@i
</option>
}
</select>
頁|
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex + 1)"
asp-route-currentFilterNumber="@ViewData["CurrentFilterNumber"]"
asp-route-CurrentFilterCustomer="@ViewData["CurrentFilterCustomer"]"
asp-route-pageSize="@ViewData["PageSize"]"
class="btn btn-outline-secondary btn-sm @nextDisabled">
下一頁
</a>

</div>
</div>
</div>
</form>


@section Scripts{
<script type="text/javascript">
function clearSearch() {
$('#input_customer').val('')
$('#input_number').val('')
$('#form_search').submit()
}
function goToPage() {
$('#form_search').submit()
}
function changePageSize() {
$('#form_search').submit()
}
</script>
}

關鍵在把整個表都包進form裡面,共用一個Action。
第10~45行:為查詢、排序
第50~119行:為列表
第125~168行:為分頁區塊
第173~184行:讓一些操作可以再改變時自動執行form的Action。

5.完成後執行畫面:

總結

這種要貼程式碼的很難去解釋,只能說照著做就能弄出一個列表。
官方 https://docs.microsoft.com/zh-tw/aspnet/core/data/ef-mvc/sort-filter-page?view=aspnetcore-3.1 也是這樣提供。
我跟官方作法差異在於:排序可以排多個、過濾可以過濾多個、有分頁前往的方式。
總之就是大方向是對的:建立查詢>過濾>排序>分頁。細節再修正成自己需求。

然後,其實用API套個前端列表會比較簡單,撈出全部然後交給前端表格元件。
但後端這種則比較穩定一些,因為編譯時就能知道錯誤,需要比較多程式碼才能達到。

最後,讓我比較訝異的是竟然找不到現成的非官方範例可以參考…。

完整程式碼:https://github.com/yuhsiang237/ASP.NET-Core-MVC-PaginatedList

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/data/ef-mvc/sort-filter-page?view=aspnetcore-3.1
https://blog.csdn.net/mzl87/article/details/102924701
https://jscinin.medium.com/asp-net-core-mvc-part-12-%E7%82%BA%E9%99%B3%E5%88%97%E7%9A%84%E8%B3%87%E6%96%99%E8%A3%BD%E4%BD%9C%E5%88%86%E9%A0%81%E6%95%88%E6%9E%9C-%E5%A5%97%E4%BB%B6x-pagedlist-core-mvc-a2191d86317d
https://docs.microsoft.com/zh-tw/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/sorting-filtering-and-paging-with-the-entity-framework-in-an-asp-net-mvc-application

[Day16] C# MVC Dapper用法與連結資料庫 - C#&AspNetCore

在上回 [Day15] C# MVC LINQ常見用法 - C#&AspNetCore ,我們介紹了LINQ常見用法。

發現LINQ在組合複雜的查詢比較困難,為了解決這困難通常會用原生寫法替代。
因此,這回我們用另一種方式連結資料庫,Dapper。

此外,如果是用Entity Framework core可以參考之前寫的這篇喔:[Day7] C# MVC Model模型連結資料庫 - C#&AspNetCore

Dapper

Dapper是適用於Microsoft .NET平台的對象關係映射產品:它提供了將面向對象的域模型映射到傳統關係數據庫的框架。其目的是將開發人員從大部分與關係數據持久性相關的編程任務中解放出來。

安裝

1.用Nutget

2.搜尋Dapper安裝

實作

先列出異動的檔案目錄,黃色為我們會改的檔案:

1.首先就是要建立Dapper連結資料庫的類別

~/Dapper/DataControl.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using Dapper;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;

namespace DapperExample.Dapper
{
public class DataControl : IDisposable
{
private IDbConnection db = null;
private string _connectStr;

//建構子初始化連線字串/DB連接
public DataControl()
{
// 自行替換資料庫連結字串
string connectionStr = @"Server=.\SQLExpress;Database=Orders;Trusted_Connection=True;ConnectRetryCount=0";
_connectStr = connectionStr;
db = new SqlConnection(_connectStr);
}

//共用查詢方法
public IEnumerable<T> Query<T>(string sql, object param)
{
return db.Query<T>(sql, param).ToList();
}

//共用新增刪除修改方法
public int Execute(string sql, object param)
{
return db.Execute(sql, param);
}

//釋放連接
public void Dispose()
{
db.Dispose();
}
}
}

說明:
唯一要改的在20行要套入自己的資料庫連結字串。

2.再來建立對應的資料表Model

~/Models/Customer.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.ComponentModel.DataAnnotations;
namespace DapperExample.Models
{
public class Customer
{
[Display(Name = "編號")]
public string Number { get; set; }

[Display(Name = "名子")]
[Required(ErrorMessage = "名字不可為空")]
public string Name { get; set; }

[Display(Name = "電話")]
public string Tel { get; set; }
}
}

說明:依照資料表去訂定自己的Model。

3.在Controller使用

~/Controllers/HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using DapperExample.Dapper;
using DapperExample.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace DapperExample.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
Customer customer;
// 使用using,區塊結束後會自動執行Dispose
using (DataControl dc = new DataControl())
{
string number = "U001";
string sql = @"SELECT * FROM Customers WHERE number = @number";
var result = dc.Query<Customer>(sql, new { number });//執行DataControl的Query方法
customer = result.First();
}
return View(customer);
}

public IActionResult Privacy()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

說明:
在26~32行使用using是Dapper的作用區域,離開後Dapper會自己Dispose。
注意:只有用using才會自動Dispose
我們把資料查出來後抓第一筆,設定到Customer Model,並傳給View。

4.呈現在畫面上
~/Views/Home/Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@model DapperExample.Models.Customer;
@{
ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
<ul>
<li>@Model.Number</li>
<li>@Model.Name</li>
<li>@Model.Tel</li>
</ul>

5.完成,網頁上結果如下:

資料庫資料:

結論

Dapper配置非常簡單!整體安裝到撰寫連線不用15分鐘。
更多的文件可以參考:https://dapper-tutorial.net/dapper
主要在複雜查詢、提升效能可以用,而普通的新/刪/修/查還是用Entity Framework core強型別會好一些。

如果是用Entity Framework core可以參考之前寫的這篇:[Day7] C# MVC Model模型連結資料庫 - C#&AspNetCore

參考資料
https://blog.darkthread.net/blog/dapper/
https://ithelp.ithome.com.tw/articles/10240341

[Day15] C# MVC LINQ常見用法 - C#&AspNetCore

在上回 [Day14] C# MVC LINQ基礎用法 - C#&AspNetCore ,我們介紹了LINQ基本用法。

而這回就要介紹LINQ常用的幾個JOIN、LEFT JOIN作法。

實作

先附上測試資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 產品
Product[] products = new Product[] {
new Product { Id = 1 ,Name = "PS4",Price =11000 } ,
new Product { Id = 2 ,Name = "PS5",Price =15000 },
new Product { Id = 3 ,Name = "Switch",Price=9900 },
new Product{ Id= 4 ,Name="XBOX Series X",Price=15000 }
};

// 分類
ProductType[] productTypes = new ProductType[] {
new ProductType {Id = 1 ,Name = "Game"} ,
new ProductType { Id = 2 ,Name = "nintendo" } ,
new ProductType { Id =3 ,Name = "Sony" }
};


// 產品與分類關系表
Product_ProductTypeRelationship[] product_ProductTypeRelationships = new Product_ProductTypeRelationship[] {
new Product_ProductTypeRelationship {
ProductID = 1 ,ProductTypeID = 1
} ,
new Product_ProductTypeRelationship {
ProductID = 1 ,ProductTypeID = 3
} ,
new Product_ProductTypeRelationship {
ProductID = 2 ,ProductTypeID = 1
} ,
new Product_ProductTypeRelationship {
ProductID = 2 ,ProductTypeID = 3
},
new Product_ProductTypeRelationship {
ProductID = 3 ,ProductTypeID = 1
} ,
new Product_ProductTypeRelationship {
ProductID = 3 ,ProductTypeID = 2
}
};

1.LINQ實作INNER JOIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void linqInnerJoinExample()
{
var query =
from product in products
join product_ProductTypeRelationship in product_ProductTypeRelationships on product.Id equals product_ProductTypeRelationship.ProductID
join productType in productTypes on product_ProductTypeRelationship.ProductTypeID equals productType.Id
select new
{
product.Id,
product.Name,
product.Price,
productTypeName = productType.Name
};
var result = query.ToList();
}

結果:

說明:因為是InnerJoin,都要有重疊才會關聯出,而XBOX Series X的Id沒有在product_ProductTypeRelationships裡面,所以最後沒有被顯示出來。
基本上跟原生SQL的寫法挺像的。

2.LINQ實作LEFT JOIN

寫法1:拆兩個Query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void linqLeftJoinExample()
{
// product 跟 product_ProductTypeRelationship 先left join
var query1 =
from product in products
join product_ProductTypeRelationship in product_ProductTypeRelationships on product.Id equals product_ProductTypeRelationship.ProductID
into tmp
from product_ProductTypeRelationship in tmp.DefaultIfEmpty()
select new
{
product.Id,
product.Name,
product.Price,
ProductTypeID = product_ProductTypeRelationship?.ProductTypeID
};

// query1結果再跟 productTypes left join
var query2 =
from q in query1
join productType in productTypes on q.ProductTypeID equals productType.Id
into tmp
from product_ProductTypeRelationship in tmp.DefaultIfEmpty()
select new
{
q.Id,
q.Name,
q.Price,
ProductName = product_ProductTypeRelationship?.Name
};
var result = query2.ToList();
}

結果:

說明:可以得到XBOX Series X了。
因為LINQ沒有LEFT JOIN這種簡單作法,只能靠DefaultIfEmpty來操作,作法比較複雜、難直覺判斷。
((ps.不是要用來讓大家好用的查詢嗎?搞複雜了

作法2:組多個DefaultIfEmpty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void linqLeftJoinExample2()
{
var query =
from a in products
join b in product_ProductTypeRelationships on a.Id equals b.ProductID
into result1
from ab in result1.DefaultIfEmpty()
join c in productTypes on ab?.ProductTypeID equals c.Id
into result2
from abc in result2.DefaultIfEmpty()
select new{
a.Id,
a.Name,
a.Price,
ProductName = abc?.Name
} ;
var result = query.ToList();
}

結果:

說明:可以得到XBOX Series X了。
這種多個DefaultIfEmpty組法感覺蠻難讀的。
要獨立出一個值,導致得用a、b、c來組不然難知道誰是誰,就很難閱讀
且要小心有些值讀出來是空的要使用「?.」的做法,把empty的值設定為null才有辦法繼續下去,否則會執行到中間就報錯了。

以此,就是ab?.ProductTypeID,因為ab做完DefaultIfEmpty時可能ProductTypeID是為empty的,要用這ProductTypeID再去做下個join可能會出錯。

3.組成階層方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void linqLeftJoinExample3()
{

var p = (from a in products
select new ProductViewModel
{
Id = a.Id,
Name = a.Name,
Price = a.Price,
}).ToList();

foreach (var item in p)
{
var productTypeIDs = from a in product_ProductTypeRelationships
where a.ProductID == item.Id
select a.ProductTypeID;

var productTypeData = from a in productTypeIDs
join b in productTypes on a equals b.Id
select new ProductType
{
Id = b.Id,
Name = b.Name
};
item.ProductTypes = new List<ProductType>();
foreach (var x in productTypeData)
{
item.ProductTypes.Add(x);
}
}
var result = p;
string jsonData = JsonConvert.SerializeObject(result);
System.Diagnostics.Debug.WriteLine(jsonData);
}

結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[
{
"Id": 1,
"Name": "PS4",
"Price": 11000,
"ProductTypes": [
{
"Id": 1,
"Name": "Game"
},
{
"Id": 3,
"Name": "Sony"
}
]
},
{
"Id": 2,
"Name": "PS5",
"Price": 15000,
"ProductTypes": [
{
"Id": 1,
"Name": "Game"
},
{
"Id": 3,
"Name": "Sony"
}
]
},
{
"Id": 3,
"Name": "Switch",
"Price": 9900,
"ProductTypes": [
{
"Id": 1,
"Name": "Game"
},
{
"Id": 2,
"Name": "nintendo"
}
]
},
{
"Id": 4,
"Name": "XBOX Series X",
"Price": 15000,
"ProductTypes": []
}
]

說明:用foreach搭配LINQ用組的方式去做出階層資料,並且新增ProductViewModel做最後的資料規格呈現。
這種複雜度O(n^2)不是很好,未來想到好的方式再更改。
且要注意第10行要先ToList(),執行LINQ,否則下面的foreach會造成每次初始化。
如果以前端要資料來講,這樣是最好格式,可以不用手動組。
此外,這是多對多的資料關聯,是最複雜的情況。

總結

介紹了資料常見的處理JOIN、LEFT JOIN。
覺得要處理複雜查詢用LINQ真的不太適合…,因為太難閱讀XD?。
LINQ有他強型別、Intellisense的好處,但想到有些情況要LEFT JOIN 5張時應該會蠻崩潰的。
可能就得組簡單的LINQ再搭配迴圈分步驟做,效能會差一點。

總之,在複雜的查詢我會用「原生作法搭配Dapper」或是「簡單的LINQ搭程式用迴圈做」。

更多LINQ用法可以查看:
https://docs.microsoft.com/zh-tw/dotnet/csharp/linq/

參考資料
https://dotblogs.com.tw/yc421206/2014/07/11/145907
https://ad57475747.medium.com/linq%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-6-join-%E5%A4%9A%E8%A1%A8%E5%96%AE%E5%A4%9A%E6%A2%9D%E4%BB%B6%E5%BC%8F-4076c487264f
http://vito-note.blogspot.com/2013/04/linq-3-join.html
https://docs.microsoft.com/zh-tw/dotnet/csharp/linq/perform-left-outer-joins
https://blog.csdn.net/yuanxiang01/article/details/111176654

[Day14] C# MVC LINQ基礎用法 - C#&AspNetCore

在上回 [Day13] C# MVC 驗證與授權,新刪修查按鈕權限 - C#&AspNetCore ,我們學會了進階的權限切割。

而這回就要開始操作資料了,使用LINQ。

LINQ

Language Integrated Query (LINQ) 是一組以直接將查詢功能整合至 C# 語言為基礎之技術的名稱。 傳統上,資料查詢是以簡單的字串表示,既不會在編譯時進行類型檢查,也不支援 IntelliSense。 此外,您還必須針對每種資料來源類型學習不同的查詢語言:SQL 資料庫、XML 文件、各種 Web 服務等等。透過 LINQ,查詢會是第一級語言建構,和類別、方法及事件相同。 您可以使用語言關鍵字和熟悉的運算子,針對強型別的物件集合撰寫查詢。 LINQ 技術系列會針對物件 (LINQ to Object)、關聯式資料庫 (LINQ to SQL) 與 XML (LINQ to XML),提供一致的查詢體驗。

對於撰寫查詢的開發人員來說,LINQ 最明顯的「語言整合」部分就是查詢運算式。 查詢運算式是以宣告式「查詢語法」撰寫。 透過使用查詢語法,您就可以利用最少的程式碼,針對資料來源執行篩選、排序及分組作業。 您可以使用相同的基本查詢運算式模式來查詢和轉換 SQL 資料庫、ADO.NET 資料集、XML 檔和資料流程和 .net 集合中的資料。

您可以使用 C# 針對下列項目撰寫 LINQ 查詢:SQL Server 資料庫、XML 文件、ADO.NET 資料集,以及支援 IEnumerable 或泛型 IEnumerable 介面的任何物件集合。 也有協力廠商針對許多 Web 服務和其他資料庫實作提供 LINQ 支援。

所有 LINQ 查詢作業都包含三個不同的動作:

  1. 取得資料來源。
  2. 建立查詢。
  3. 執行查詢。

實作

HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public IActionResult Index()
{
// Specify the data source.
int[] scores = new int[] { 97, 92, 81, 60 };

// Define the query expression.
IEnumerable<int> scoreQuery =
from score in scores
where score > 80
select score;

// Execute the query.
foreach (int i in scoreQuery)
{
Console.Write(i + " ");
}
return View();
}

說明:
以上就是過濾>80的分數。

  • IEnumerable:第7行,公開能逐一查看非泛型集合內容一次的列舉程式。而其實可以用var當作宣告替代。
  • scoreQuery:過濾的結果。 下圖顯示完整的查詢作業。

總結

本人習慣直接操作SQL語言,但LINQ說實在也不錯(可以直接防SQL INJECTION、強型別的優勢)。
但感覺上LINQ比較適合簡單的查詢,變成在程式裏面爬出來後去操作,性能可能會低,但比較穩。
而對於複雜的SQL查詢還是有困難QQ,變成翻譯家的感覺。
基於以上原因,複雜查詢我應該會使用Dapper來做,寫原生。

此外,以有寫過Laravel跟Java的經驗來說,LINQ是C#微軟Only。
也就表示熟悉LINQ可能會有換其他語言SQL卡住、忘記正常該怎麼組的問題。

在這篇中先小試一下,下篇將注重一些平常可能會做的LINQ查詢操作。

參考資料
https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/linq/
https://ithelp.ithome.com.tw/articles/10194468
https://blog.darkthread.net/blog/linq-or-direct-sql/

後端API適合共用APP與WEB嗎?

先說結論:不建議。

因為會出現以下問題:

  1. 開發時程問題:因網站改API,APP就必須要更新API。
  2. 難同時開發:可能先做完WEB才會做APP,或反過來,彼此資料也差異大。
  3. 難同時滿足兩者:畫面差異大,後端不知道APP或WEB要底要哪種基礎資料,可能少或多給。
  4. 討論時間拉長:因為第3點肯定會延長開會討論,也延遲到開發。

此外,如果APP要向下支援,不強制更新,那會需要API版號,如v1、v2…等,可以塞在header裡面。
而向下支援可能就會有資料庫table異動與欄位被刪,像是沒辦法新增、編輯,這時僅能提示使用者安裝新版本了。
如果v1版要跟v100版間要相容肯定會處理的蠻崩潰的。

基於以上理由,WEB、APP分兩組API是比較好長期維護、開發的方法。

最後不推graphQL,個人工作經驗只會讓專案變得難以維護。
然後API一定要RESTful嗎?
這也不一定,有時分權而難以限定權限,就會有漏洞。反而增加開發風險。
雖然有人會覺得RESTful就是主流,潮。但然後呢…。Restful很簡潔,能讓知道這模式的人容易上手,但是否必要要看開發者所在的環境而定。
可參考以下參考資料,有篇提到KISS (Keep It Simple, Stupid)。

參考資料
https://www.ptt.cc/bbs/Soft_Job/M.1624876117.A.086.html
https://blog.darkthread.net/blog/is-restful-required/