Arduino限流LED實驗

前言

想以僅有的材料做個LED限流的實驗,並使用電路公式算出理論數值實作。

材料

  1. LED 3MM 紅色 x 1 :發光二極體
  2. Arduino版 x 1:任意一塊都可
  3. 220歐姆電阻 x 3:紅紅棕金

理論部分

首先我們Arduino電源接3.3V,而LED紅的電流需要0.02A(20mA)電壓則是2.0-2.2V,姑且算2.0電壓。

這時3.3V怎樣降到2.0V?就是使用電阻
那要多少歐姆的電阻值呢?
透過歐姆定律
在電路學裏,歐姆定律(英語:Ohm’s law)表明,導電體兩端的電壓與通過導電體的電流成正比,以方程式表示。

1
V=IR

(3.3-2.0) = 0.02 * R
1.3 = 0.02R
R = 1.3/0.02
R = 65
所以我們需要65Ω(Ohm)的電阻
但天殺的= =肯定不會這麼剛好有65歐姆的電阻,這時就要用現有的電阻兜出來。
並聯電阻可以降低歐姆。
依照克希荷夫電路定律推倒後得到以下公式:

1
Rt = 1/(1/R1+1/R2+1/R3+...+1/Rn)

以現有的220歐姆電阻要降到65就要稍微計算一下

3顆:
1/(1/220+1/220+1/220) = 73
4顆:
1/(1/220+1/220+1/220+1/220) = 55

發現3顆電阻約可以到73很接近65了,第4顆則是會低過65。
我們使用電阻是為了降低電壓、限流,讓IC能夠正常啟用下避免過熱燒毀、穩定運作,所以差不多就使用3顆電阻並聯。

電路圖

接線圖

現實:

總結

用理論公式推後就可以用任意的電壓、電阻來兜
避免IC燒壞、延長使用時間

((ps.懂了後會覺得一些範例書上只給一個電阻然後不告訴你為什麼、怎麼算實在太黑了。

參考資料
電阻色碼計算
https://zh.wikipedia.org/wiki/%E9%9B%BB%E9%98%BB%E8%89%B2%E7%A2%BC
電阻放置
https://zhidao.baidu.com/question/1610875004126323107.html
限流電阻計算器
https://gc.digitw.com/Program/Resistor4LED/Resistor4LED.htm
為什麼LED要加上電阻
http://yehnan.blogspot.com/2012/03/arduinoled220-ohm.html
控制LED
https://blog.jmaker.com.tw/arduino-tutorials-3/
歐姆定律
https://zh.wikipedia.org/wiki/%E6%AC%A7%E5%A7%86%E5%AE%9A%E5%BE%8B
並聯電路
https://zh.wikipedia.org/wiki/%E4%B8%A6%E8%81%AF%E9%9B%BB%E8%B7%AF#%E9%9B%BB%E9%98%BB%E5%99%A8
克希荷夫電路定律
https://zh.wikipedia.org/wiki/%E5%9F%BA%E7%88%BE%E9%9C%8D%E5%A4%AB%E9%9B%BB%E8%B7%AF%E5%AE%9A%E5%BE%8B#%E5%9F%BA%E7%88%BE%E9%9C%8D%E5%A4%AB%E9%9B%BB%E6%B5%81%E5%AE%9A%E5%BE%8B%EF%BC%88KCL%EF%BC%89
歐姆定律
https://physcourse.thu.edu.tw/mengwen/%E6%99%AE%E7%89%A9%E5%AF%A6%E9%A9%97/%E5%AF%A6%E9%A9%97%E9%A0%85%E7%9B%AE/%E6%AD%90%E5%A7%86%E5%AE%9A%E5%BE%8B/

[Day24] C# MVC API JWT Token驗證 (上) JWT基礎知識 - C#&AspNetCore

在先前有介紹過Cookie-Based驗證於MVC專案裡。

[Day12] C# MVC 驗證與授權,登入與登出 - C#&AspNetCore

而這回主要探討的是Token-Based驗證,也就是API常用的JWT (Json Web Token)。

什麼是JWT

JSON Web Token (JWT) is a proposed Internet standard for creating data with optional signature and/or optional encryption whose payload holds JSON that asserts some number of claims.

為JSON Web Token,是一個建議的網路標準,用來建立簽名,且在其中可以視需求加入自訂的JSON內容。

簡言之,就是令牌(Token),你可以拿這令牌去要資料。

JWT優點

跨域: 不受網域限制,可用來串接第三方應用,如 OAuth。
安全性: 不使用 Cookie 因此不會受到 CSRF 攻擊,不過 Token 並不能防護 XSS 攻擊,還是需要特別注意。
行動端: 可用於不支援 Cookie 的裝置上,且現在網站和 APP 串接普遍使用 Token 授權。

JWT格式

主要由三個部分組成:

1
header.payload.signature

Header

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

alg:定義加密的方式產生signature,如上是HS256表示使用是HMAC-SHA256.
typ:Token類型

常被使用的header如:typ、cty、alg、kid、x5c、x5u、crit
可自行添加參考:https://en.wikipedia.org/wiki/JSON_Web_Token

Payload

1
2
3
4
{
"loggedInAs": "admin",
"iat": 1422779638
}

loggedInAs:這是客製化claim,以此來說是管理員。
iat:這是標準的claim,表示此JWT發布的時間。

標準的claim如:iss、sub、aud、exp、nbf、iat、jti
可自行添加參考:https://en.wikipedia.org/wiki/JSON_Web_Token

通常為了讓前端或後端好存取多加上一些識別值。

Signature

1
2
3
4
5
HMAC_SHA256(
secret,
base64urlEncoding(header) + '.' +
base64urlEncoding(payload)
)

簽名,確保資料完整性的雜湊簽章。

將以上三者用Base64url Encoding結果即為JWT

生成:

1
const token = base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)

結果:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

使用情境

將此Token放置於API請求的Header中:
例如:

1
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

傳送給後端

JWT衍伸問題

Token的竊取問題,因為JWT發出去就不能銷毀,只能自己等待過期。
所以有了Refresh Token的出現。

使用Refresh Token後:
縮短每個Token時間,過期時再重新索取。

1
2
3
4
5
{
"access_token":"l0XG52TQx", //Token
"refresh_token":"KWI3JOkFA", //Refresh Token
"expires_in":3600 //幾秒過期
}

當Token過期後,使用refresh_token呼叫一個API要取一個新Token。
refresh_token是存在自己資料庫內的,所以被盜用可以馬上刪除掉!

在寫程式碼時可以在某API發現過期後立即呼叫更換。
或是用Token裡面帶入的payload去確認更換時機也可以,但可能會有時區問題。

此外,如果用Refresh Token仍會有一些安全疑慮,就是發出的token還是要等到時間自己逾期。
(不如自己建表記Token還比較安全,所以在下面我寫了額外補充)

額外補充

除了JWT外也有一些自訂的做法能用來驗證APP手機端。
如DB自建Token表,可能像是這樣:

每次操作API都帶入Token,DB檢驗一次。
被盜用時想刪隨時能刪、能強制登出。

Tokens資料表

UserID Token Expires_In
1 AJIASOIASssarJ 2021-10-16
2 AASKOPASKOAPaS 2021-10-17
3 AASK45as4daaS 2021-10-14

流程大概像這樣:
API>撈DB檢查>大於時間,刪除Token>回傳授權失敗

雖然比較耗資料庫資源但我覺得是最安全的方式XD。
而且沒有發出要等到時間到才會逾期的問題

總結

在這回中主要探討JWT是什麼、前置的知識理論。
發現了JWT的一些好處與壞處。

因此如果有疑慮好像自己建立Token表還是最安全的XD

而在下篇中將會實作JWT的機制。

參考資料
https://ithelp.ithome.com.tw/articles/10199102
https://en.wikipedia.org/wiki/JSON_Web_Token

[Day23] C# MVC Web API版本增加Swagger - C#&AspNetCore

在上回中我們介紹了 [Day22] C# MVC API版本控制 - C#&AspNetCore ,實作了RESTful API的版控。

而這回將要進行API的部屬,使用Swagger UI。

找了很多篇算是找到一篇
https://codingfreaks.de/dotnet-core-api-versioning/
可以同時使用API Versioning跟Swagger套件的範例

在這篇中主要配置API並且切版本

1.先用Nuget安裝以下套件:
Swashbuckle.AspNetCore
Swashbuckle.AspNetCore.Annotations
Swashbuckle.AspNetCore.Swagger
Swashbuckle.AspNetCore.SwaggerGen
Swashbuckle.AspNetCore.SwaggerUI

2.再來是我們要修改的檔案

首先是Swagger的定義類別

~/ConfigureSwaggerOptions.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
namespace ApiVersioningSample
{
using System;
using System.Linq;

using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;

using Swashbuckle.AspNetCore.SwaggerGen;

/// <summary>
/// Configures the Swagger generation options.
/// </summary>
/// <remarks>
/// <para>
/// This allows API versioning to define a Swagger document per API version after the
/// <see cref="IApiVersionDescriptionProvider" /> service has been resolved from the service container.
/// </para>
/// <para>Taken from https://github.com/microsoft/aspnet-api-versioning.</para>
/// </remarks>
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
#region member vars

private readonly IApiVersionDescriptionProvider _provider;

#endregion

#region constructors and destructors

/// <summary>
/// Initializes a new instance of the <see cref="ConfigureSwaggerOptions" /> class.
/// </summary>
/// <param name="provider">
/// The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger
/// documents.
/// </param>
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
{
_provider = provider;
}

#endregion

#region explicit interfaces

/// <inheritdoc />
public void Configure(SwaggerGenOptions options)
{
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}

#endregion

#region methods

/// <summary>
/// Internal implementation for building the Swagger basic config.
/// </summary>
/// <param name="description">The description object containing the.</param>
/// <returns>The generated Open API info.</returns>
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo
{
Title = "Sample API",
Version = description.ApiVersion.ToString(),
Description = @"<p>Sample API with versioning including Swagger.</p><p>Partly taken from <a href=""https://github.com/microsoft/aspnet-api-versioning"">this repository</a>.</p>",
Contact = new OpenApiContact
{
Name = "Yu Hsiang",
}
};
if (description.IsDeprecated)
{
info.Description += @"<p><strong><span style=""color:white;background-color:red"">VERSION IS DEPRECATED</span></strong></p>";
}
return info;
}

#endregion
}
}

這類別主要用來放Swagger客製化的數值

再來是啟用Swagger

~/Startup.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
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using TodoAPI.Models;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.Extensions.Options;
using ApiVersioningSample;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace TodoAPI
{
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.AddControllers();
// 資料庫配置
var connection = @"Server=.\SQLExpress;Database=TodoDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<TodoDBContext>(options => options.UseSqlServer(connection));

// 啟用API版控功能
services.AddApiVersioning(
options =>
{
// reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;
});

// swagger 配置
services.AddVersionedApiExplorer(
options =>
{
// add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
// note: the specified format code will format the version as "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat
// can also be used to control the format of the API version in route templates
options.SubstituteApiVersionInUrl = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen();


}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

// swagger 配置
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
});


app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();

});


}
}
}

第48~60行:swagger 配置
第79~89行:swagger 配置

4.版本的類別撰寫:

~/Controllers/V1_1/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.V1_1
{
[ApiVersion("1.1")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v1.1" };
}
}
}

~/Controllers/V1_1/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_1
{
[ApiVersion("1.1")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v1.1" };
}
}
}

~/Controllers/V2_0/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.V2_0
{
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VersionController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "api v2.0" };
}
}
}

基本上就是改
第4行資料夾:V1_0 至 V1_1 至 V2_0
第6行ApiVersion:1.0 至 1.1 至 2.0
其他的Controller也是一樣,這邊就舉例VersionController.cs

5.啟用後範例:

專案連結

https://github.com/yuhsiang237/ASP.NET-Core-RESTfulAPI-Swagger

總覽

Swagger UI搭配版控,而在.net Core官方是沒有提供版本+Swagger配置的,所以踩很多雷。
這回參考了 https://codingfreaks.de/dotnet-core-api-versioning/ 的文章。
花了不少時間在除錯,最後終於把Swagger搭建起來。

參考資料
https://codingfreaks.de/dotnet-core-api-versioning/
https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-3.1&tabs=visual-studio
https://medium.com/wenchin-rolls-around/%E8%BB%9F%E9%AB%94%E7%89%88%E6%9C%AC%E8%99%9F-ee446e1ad543

[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