近期在大量以 gRPC 為基礎的開發上,很多時機需要新增或是編輯修改 .proto 檔案.
而在未安裝任何套件前當前版本的 VS Code 預設不支援 .proto
的程式碼高光(highlight)與程式碼智能快選(code intellisense)
這邊推薦安裝 vscode-proto3
安裝完後即可看到 VS Code 有對應的檔案 icon 與 程式碼高光功能了
全部支援的功能包含:
另外還要推薦一個 Protobuf Helper 可以自動幫我們填入 field id.
由於 protobuf 撰寫定義檔時,針對 message 的每一個 field 都要明確的給定 id,其實大多開發人員都不在意這點但又覺得很擾人,這時此套件就可以幫你快速填上或是重整摟.
即可馬上變成下面:
這個套件再多屬性的 message 開發中,根本是神器,以往不敢亂改的 field 順序現在就可以隨便移動摟
]]>從 Consul 官方的文件中或是 Docker Hub
會叫你用以下指令就可以開啟 Development mode
docker run -d --name=dev-consul -e CONSUL_BIND_INTERFACE=eth0 consul
官網還寫上補上 -ui
就可以開啟 UI 畫面.
試了幾次不行後才眼殘發現要將預設的 0.0.0.0 綁定才能成功連到預設的 8500 port
docker 指令時記得補上 0.0.0.0
:
docker run --name dev-consul -d -p 8500:8500 consul agent -dev -ui -client=0.0.0.0 -bind=0.0.0.0
接者就可以成過連線至 http://localhost:8500
延續先前兩篇 gRPC 應用程式 - 基礎入門 與 gRPC 的 GUI 工具 - BloomRPC 協助 .NET Core 開發 gRPC 應用程式的整合測試 後,這篇要介紹一下Grpc.Tools 1.17.0
之後整合 .csproj 的開發方式如何更加輕鬆地進行 gRPC 的開發.
以下內容是延續 gRPC 應用程式 - 基礎入門 做分享,建議先看此篇後再往下閱讀.
範例程式碼:blackie1019/demo-grpc-dotnet-build-integration
所以我們先產生了與之前雷同的目錄與內容:
1 | / |
與先前不同的是本次直接將 gRPC 的開發整合進入 Demo.Message.csproj:
1 | <Project Sdk="Microsoft.NET.Sdk"> |
另外,本次專案這邊額外定義了一個通用類別於 Common 目錄下,並於使用到該類型的 .proto 載入此檔案(路徑需包含 subfolder).
而 Grpc.Tools 設定了 PrivateAssets="All"
,該屬性指定應取用哪些資產不會放入下一個專案的套件,阻止下一個引用該專案產生的 .dll 時 .csproj 的 .dll 引用繼承。
細節可參考Package references (PackageReference) in project files
只要設定此步驟即可完成編譯,而這邊我們選擇 CompileOutputs 為 False
則需要手動IDE編譯:
如果編譯有問題的朋友建議可以進入專案目錄下透過 dotnet build
編譯產出時查看細部編譯執行紀錄:
當將 Server 與 Client 兩者運行即可看到以下畫面:
透過 Grpc.Tools 1.17.0
與 .csproj 的直接整合,即可加入原先手動執行 protoc 的麻煩與人為錯誤的可能,加速 gRPC 於 .NET 上的開發效率.
密碼加密是常見的開發需求,一般最簡單的做法可能會有直接將密碼明文(Plain Text)直接透過雜湊演算法( MD5 或是 SHA-1 )進行加密產生密碼密文(Ciphertext),
最後將密文儲存至DB 之後輸入每次都接受明文後透過相同流程產生密文後比對
兩者是否相同.
但上面的做法雖然可以在資料層不暴露密碼的明文,但對於密碼的保護絲毫沒有任何效果.這兩者並不安全且所生成的Hash值也是相當的薄弱。它主要的優點在於生成速度快且易於實現。但是,這也意味著它是容易被暴力攻擊(Exhaustive attack)和字典攻擊(Dictionary Attack)。例如使用明文和Hash生成的彩虹表(Rainbow table)可以快速地搜索已知Hash對應的原使明文。
此外,MD5並沒有避免Hash碰撞:這意味不同的密碼會導致生成相同的Hash值。
如果真的要採用以上方法的話建議針對密碼加鹽(Salt),每次密碼生成時增加一段亂數產生的資料並合併至原始明文內進行雜湊:
以上的做法可以有效地產生避免上述問題,但無法解決暴力攻擊的直接破解,尤其有心人士可能都是拿GPU來做更高速的運算,此時你就需要專門為了網路應用程式開發的密碼而生的 PBKDF2 來生成密碼.
PBKDF2(Password-Based Key Derivation Function) 是一個用來產生密鑰的雜湊函數,常用於生成具備加密的密碼。
它的基本原理是通過一個偽隨機函數(例如HMAC函數),把明文和一個鹽值作為輸入參數,然後重複進行運算,並最終產生密鑰
PBKDF2 的定義如下:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
DK(derived key)
產生的密碼金耀
PRF(pseudo-random function)
表示是一個隨機函數,例如HASH_HMAC函數,它會輸出長度為hLen的的結果。
Password
輸入的密碼明文
Salt
隨機產生的一次性亂數(長度須高於一定長度)
c
進行重複計算的回合數(越多越複雜)
dkLen
欲產生的 DK 密文金鑰長度
如果重複計算的回合數足夠大,破解的成本就會變得很高。而Salt的添加也會增加攻擊的難度。上述的流程則如下示意:
PBKDF2 與其他知名的密碼雜湊比較(Bcrept, SHA-256)的破解成本比較就可以知道差異:
另外因為GPU比CPU做運算會快上更多,而PBKDF2 有針對 GPU
的攻擊做防範,這則是傳統加密雜湊運算所沒有的,整體 GPU 運算比較如下(oclHashcat-plus):
現金的無線網路 WPA/WPA2 的規格預設採用 PBKDF2,所以各語言的普及率也高,如果需要找個密碼標準來用的話,PBKDF2 是相當不錯的選擇.
開發上只要引入 KeyDerivation
這個 Class
Namespace:
Microsoft.AspNetCore.Cryptography.KeyDerivation
Assembly:
Microsoft.AspNetCore.Cryptography.KeyDerivation.dll
因為是 ASP.NET Core 預設的已包含的套件,開發 ASP.NET Core 的朋友可以直接使用,而開發 .NET Core 的朋友則需要透過引入指令以下加入專案參考:
Install-Package Microsoft.AspNetCore.Cryptography.KeyDerivation
這邊需要呼叫的是 KeyDerivation.Pbkdf2(String, Byte[], KeyDerivationPrf, Int32, Int32) Method:
password
原始傳入的密碼明文
salt,
藉由Encoding.UTF8.GetBytes轉成byt陣列資料,傳進Pbkdf2參數,當鹽使用。
prf,
PRF是一個偽隨機函數
iterationCount
雜湊執行次數
numBytesRequested,
產出的 DK 長度
程式碼如下:
1 |
|
有開發過 ASP.NET Core 的朋友一定對 appsetting.json 的使用不陌生,預先支援多個環境的設定可以更方便我們在不同環境中切換
典型的設定提供者順序是:
{Environment}
是應用程式的目前裝載環境,變數值為ASPNETCORE_ENVIRONMENT
)這邊則是要分享如何使用檔案的方式讓 .NET Core 的 Console Application 透過環境變數可以載入不同環境的 configuration setting.
開發上需要載入 Microsoft.Extensions.Configuration, Microsoft.Extensions.Configuration.Abstractions 與 Microsoft.Extensions.Configuration.Json
1 | public class AppSettingsHelper |
以上即可使用 singleton 的物件將設定值透過GetValueFromKey
與指定的key
取回.
這邊在專案上如果需要同時載入 appsettings.{Environment}.json 檔案時,需要先將本機配置環境變數,使其設定可以成取得
如希望開發時能成功載入 appsettings.development.json 檔案去複寫當前的appsettings.json設定,則需有幾種方式:
啟動應用程式時透過參數額外帶入,如:
Windows
C:\> set ASPNETCORE_ENVIRONMENT=DevelopmentC:\> dotnet ...
Unix/macOS
$ export ASPNETCORE_ENVIRONMENT=Development$ dotnet ...
launch profile
帶入,如:
launchSettings.json
1 | { |
然後指行指令時透過--launch-profile
帶入參數
dotnet run --launch-profile EnvironmentsSample
IDE 設定,這邊以 Rider 為例:
這邊要特別注意 Rider 內的測試專案是另外一個設定,需要透過以下方式額外設定 Test Runner
的環境變數:
先前分享了如何開發 gRPC 應用程式,而開發完成的服務端除了自己寫客戶端/應用端去呼叫外,也可以透過 GUI 工具進行整合測試.
本此使用的工具是 BloomRPC
這邊可以直接下載安裝版的使用,安裝完成後打開的介面如下:
以先前示範的 gRPC 專案demo-grpc來做範例,我們可以先將 Demo.Server
運行起來
而後於BloomRPC的介面載入 .proto 定義,接著就可以從上面發起整合測試:
]]>gRPC 是一個由 google 開發的開源、跨語言且高效能的 RPC Framework,它可以高效地連接單個或多個數據中心的服務,也可以支持可插拔的負載均衡,追踪,健康檢查以及認證。當然,它也能應用於分散式計算的中用來連接各種設備、APP 應用、瀏覽器(需要一點技巧)與後端服務.
目前在 .NET/.NET Core 的開發上說不上方便但執行上是沒問題的,但由於步驟相對於剛入手的朋友還是有點複雜,故紀錄一下整個流程.
但在開始實作前,先補充一下一點基礎知識.
Remote procedure call (RPC) 顧名思義就是用於遠端調用, 簡單的說就是要像調用本地函數一樣請伺服器端根據輸入代為處理函數並回傳結果。
如兩台服務器A和B,A服務器上部署一個應用,B服務器上部署一個應用,A服務器上的應用想調用B服務器上的應用提供的接口,由於不在一個應用實體內,不能直接調用,所以需要通過網路來呼叫調用的方式和傳達調用所需的傳入數據。
人們最常比較 RPC 與 Restful API Restful API 現在可以說是整個網頁應用程式的主流用法,而 RPC 則是更早於 Restful API 出現的遠端調用,最簡單的差別如下:
gRPC 是由Google所開發的開源RPC Framework,可支援多種語言:C、C++、Java、Python、Go、R、Node.js、C#、Object-C、PHP 等。
透過 gRPC,可以享受如同 Restful API 呼叫一樣前後端不同語言的開發,而這也讓人常常誤會或是難分辨使用時機.
gRPC 是基於 HTTP2
以及 Protocol buffer
與 Netty
這三個很厲害的協定與技術所開發的框架.
不同於 Restful API,gRPC 提供了更加安全也穩定的取雙向的傳輸協定,比起 Restful API 單調的單方向應用更廣.且 gRPC 天生就是透過 HTTP2.0
的協定做傳輸,搭配基於 Protocol Buffers
的定義與序列化方式,將溝通用的模型與通道整合起來時效能更加提升.
但可惜的是,瀏覽器現在還不能直接跟 gRPC 伺服器溝通,所以你需要安插一個 Gateway 將請求轉到 gRPC 客戶端.
學習gRPC前,請先了解其傳輸通訊的設定檔:protocol buffers
這邊可以看到,宣告方式非常簡單,而每一個 gRPC 函式的呼叫與回傳皆為一個類別,都是需要定義的(連null
或是空值
都要宣告),如下:
1 | syntax = "proto3"; // protobuf 的版本 |
而型別轉換上(Date
與 Decimal
)是目前比較缺乏的,但如果是希望傳入的屬性有 Enum
型別也是可以宣告的,
gRPC 有四種通訊方式,以下包含在 Protobuf 中函式(function)的表達方式:
Unary RPCs,一次請求,一次返回,沒有流,這是最常用的方式:
rpc SayHello(HelloRequest) returns (HelloResponse){
}
Server streaming RPCs,客戶端發送單次請求,服務端會返回一連串的數據,比如服務端推送比賽分數的持續變化至客戶端:
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
Client streaming RPCs,客戶端會發送一連串的數據到服務端,服務端返回單次數據,比如客戶端持續發送當下的操作日誌與行為:
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
Bidirectional streaming RPCs,兩邊各自會發送一連串的數據,比如即時的語音通話以及一些遊戲場景中的互動行為:
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
應用程式與應用程式之間的通訊橋樑,一般來說開發流程為:
gRPC cli
或是寫好的 .sh
檔案來動態產生 C# 內容至gRPC通訊介面的專案所以我們先產生了以下內容目錄的內容:
1 | / |
新增一個 .sln 接著 新增第一個 .csproj 檔案 Demo.Message
,因為該專案只放定義與規格檔案,故將其設定為 .NET Standard 專案:
接著將以下的 nuget 套件安裝 Demo.Message 專案中:
然後額外新增兩個 .NET Core Console Application 專案: Demo.Clinet
與 Demo.Server
並將兩者都加入 Demo.Message 的參考
基於.NET Core 參考會直接傳入下一個加入參考的專案,這裏就不需要額外加入 Google.Protobuf 與 Grpc 的 nuget 套件了.
接著在 protos
的資料夾放入兩份檔案,傳輸的規格與方法的定義檔案:
message.proto
1 | syntax = "proto3"; |
service.proto
1 | syntax = "proto3"; |
這邊可以看到範例使用的 syntax 規格為 proto3
的定義,詳細使用可以參考Language Guide (proto3)
而要成功產生 gRPC 的 .cs 檔案, 需要先安裝 homebrew 並透過下方指令由 brew 將 gRPC 的開發工具安裝完畢(詳細參考):
brew install gRPC
安裝完成後,如果是 GO
或是其他開發語言就可以直接進行發了,而用 C# 與 .NET Core 進行開發還需要額外透過 nuget 安裝 Grpc.Tools
並且透過 nuget 將安裝至電腦的暫存區作為之後使用,安裝後的檔案會放置底下路徑內 /Users/{user}/.nuget/packages/grpc.tools/
,如下:
接下來透過gRPC的CLI工具,將.proto的檔案產生對應的C#內容至指定的專案路徑下的即可,如下指令:
1 | /Users/`whoami`/.nuget/packages/grpc.tools/1.18.0/tools/macosx_x64/protoc -I ./protos/ --csharp_out src/Demo.Message --grpc_out src/Demo.Message ./protos/*.proto --plugin=protoc-gen-grpc=/Users/`whoami`/.nuget/packages/grpc.tools/1.18.0/tools/macosx_x64/grpc_csharp_plugin |
接著在 Demo.Server
完成 DemoServiceImpl.cs 的開發,如下:
1 | namespace Demo.Server |
這邊我們可以透過 Rider 的自動產生功能帶出需要 override 的方法,加快開發流程:
完成後,即可在 program.cs 處加入以下設定並啟用服務端等待呼叫:
1 | namespace Demo.Server |
接著看到以下畫面代表服務端準備好了
最後補上客戶端的呼叫實作如下:
1 | namespace Demo.Client |
完成後並且運行起來後看到的結果如下:
如此一來gRPC的開發就完成串接呼叫了!
由於功能與效能的強大,讓 gRPC 也正式被承諾會被整合進入 ASP.NET Core 中並在 .NET Core 3 發行.
有興趣的朋友可以追一下目前的專案進度grpc/grpc-dotnet
先前分享如何使用 ZeroBrane Studio 協助 Redis 的 Lua Script 開發與除錯介紹如何正確地使用工具來幫我們除錯 Redis Lua Script.
本次結合 C#, .NET Core 與 macOS 環境,重新整理與介紹如何正確地使用 Redis Lua Script.
在大部分的開發者中,會使用 Redis 相關 framework 的人很普遍,但絕大多數僅使用這些 Framework 已包裹好的指令做操作,鮮少自己將商業邏輯包裹成一個 Lua Script
指令操作.
以 C# + .NET Core 為例,大多使用者都會使用下列的 framework 操作 Redis 內資料:
如果今天要開發的一個資料儲存的情境如下:
test
是否存在,如果不存在則在第一次呼叫的時候給予預設值 0
test
當前的值每次增加 50
以一般 Redis 的指令操作來說我們需要透過多個指令串接以上內容,這會造成 .NET Core 的程式多次進出 Redis Instance 內.
而透過 Lua Script
以上的指令可以僅透過一個客製的指令進行操作,大幅提升 Redis 效能與反應.
Redis的架構設計單執行緒的設計,在運行Lua script的時候是沒辦法處理其他的請求的,所以Lua script並不能像Database的Stored Procedure一樣運行複雜的商務邏輯,個人認為如果有以下情境可以考慮採用:
這邊開始介紹如何在 macOS 的環境開發 Redis Lua Script
大致上的內容請先參考分享如何使用 ZeroBrane Studio 協助 Redis 的 Lua Script 開發與除錯
環境使用 Docker 進行 Redis Instance 的建置,指令如下:
docker pull Redisdocker run -P --name redis-lab -d redis
透過 -P
參數,這裡進行動態的 port 配置與對應至 container 內的 6379
port
從 docker ps -a
可以查到當前配置的 port 為 32768
透過以下指令與 redis-cli
取得當前的所有鍵值
docker exec -it <container-id> bashredis-cliKEYS *
至官網下載ZeroBrane Studio 與 ZeroBranePackage/redis.lua 這個 plugin
安裝好後可配置使用者設定,載入剛下載的 plugin 至 ZeroBrane Studio 中:
mkdir $HOME/.zbstudiomkdir $HOME/.zbstudio/packages
接者將下載的 redis.lua
plugin 檔案放入剛剛建立的目錄下($HOME/.zbstudio/packages)
接著將程序打開後可以看到下圖及代表設定成功:
而開發時不要忘記要開啟 watch window
與 stack window
協助觀察變數的變化
這邊開啟應用程式後選擇 redis
則會跳出連線設定視窗,如果要重設則需要重新啟動 ZeroBrane Studio 才可以變更.
而如果開發的 Lua Script
本身有帶入參數的需求可以使用 Command Line Parameters
的設定帶入(多個參數可用空白分隔)
如需求所列,這邊我們建立的 Lua Script 如下:
test.lua1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26local targetKey = KEYS[1] -- target key for redis
local initValue = tonumber(ARGV[1]) -- initial value for target key if not exist and need to create
local incrementValue = tonumber(ARGV[2]) -- increment value for target key when exist and calling for each time
local currentValue = initValue
-- create key with 0 if key is not exist
local isNewKey = redis.call('SETNX',targetKey,currentValue)
-- add current value if key is exist
if isNewKey == initValue then
currentValue = redis.call('GET',targetKey)
if currentValue then
currentValue = currentValue + incrementValue
end
redis.call('SET',targetKey, currentValue)
end
-- return key value
return currentValue
這邊就需要傳入三個參數,所以設定上則變成:
test 為 Key,透過中間的逗號
分隔了後面兩個 Arg ,分別為 0 與 10
而介面上簡單介紹如下(詳細介紹可參考如何使用 ZeroBrane Studio 協助 Redis 的 Lua Script 開發與除錯):
這邊最後給一段 Sample Code 說明如何透過預先載入的方式避免執行重複的 Redis Lua Script 所造成的內存耗盡問題.
其實 Redis 本身就有這個做法,可透過 SCRIPT LOAD 產生一組 SHA 的編碼後,透過 SHA 與 EVALSHA 來執行.
所以這邊的 C# 與上面已經寫好的 test.lua 的呼叫如下:
program.cs
1 | using System; |
執行結果如下:
這段程式碼還有呼叫了Load()
,這會把Lua script先載入指定的Redis server
一般指定 Master 就可以了,會自動鏡像備份到Slave。
載入後會拿到一個SHA1的 hash code,之後執行時只需傳入這個code,不需重傳整份Lua script,對需要頻繁執行的script有效能上的幫助。
]]>延續上篇MariaDB/MySQL dump SQL for Docker/Container , 在協同開發裡面最常遇到的狀況就是需要還原資料到本機搭配程式運行,如何將資料匯出與資料建立用最快的方式運行則是今天分享的內容.
這邊的前置作業請參考上一篇的 建立 MariaDB 環境與資料
本次要透過 docker-compose 加速整個流程的進行,會建立以下目錄與內容:
/Database/
|——-/dump/
|——-|– 00_init.sql
|——-|– 01_backup.sql
|– build.sh
|– docker-compose.yml
當我們有資料後則透過以下方式進行資料還原與準備初始化:
資料還原的語法(與上一篇相同),只是我們放入 build.sh 中:
1 | ! /bin/sh |
這邊要注意
準備 init.sql 如下
1 | CREATE USER 'blackie'@'%' IDENTIFIED BY 'pass.123'; |
這邊準備的 docker-compose.yml
1 | version: '2' |
為了避免 port 重複,將對外的 port 改置 3316
這邊透過 MySQL 與 MariaDB 的 docker-entrypoint-initdb.d 這個特殊的目錄,在 Instance 啟動時就幫我們執行資料目錄內的 .sh, .script 與 .sql 檔案.
另外,如果透過 docker-compose 的方式啟動記得要補上 stdin_open 與 tty 設定,確保服務不會執行完就終止.
以上資料都準備好後,只需執行 build.sh 指令集即可幫我們將資料放入新的本機容器內運行.
我們可以透過以下指令確認當前運行的環境狀態:
docker-compose ps
或是也可以透過下方指令查看所有 container 狀態:
docker ps -a
]]>使用 docker 運行 MySQL/MariaDB 已經漸漸常為開發常態,而如何將運行中的容器內 db schema 與 data 匯出至另外一個 cotainer 的 DB Instance 內則是協同開發必備的流程.
這邊簡單記錄如何從一個 MariaDB 匯入至另外一個 MySQL container 當中.
這邊先透過以下指令下載 MariaDB image 檔案並簡單建立 db schema 與 table
拉取 MariaDB image
docker pull mariadb
運行一個 Instance 並設定密碼與 port 對外
docker run -p 3306:3306 --name lab-mariadb -e MYSQL_ROOT_PASSWORD=pass.123 -d mariadb
建立資料如下:
接著透過以下步驟建立 MySQL 環境:
拉取 MariaDB image
docker pull mysql
運行一個 Instance 並設定密碼與 port 對外
docker run -p 3316:3306 --name test-mysql -e MYSQL_ROOT_PASSWORD=pass.123 -d mysql
這邊我們避免 MySQL 與 MariaDB 的 port 衝突,所以設定到 3316 ,到這邊就可以進入本篇的重點,資料匯入與匯出了.
這邊可以透過下方指令簡單匯出匯入:
從 MariaDB 備份至 back.sql :
docker exec <containerid> /usr/bin/mysqldump -B <schema-name> --routines -u root --password=pass.123 <schema-name> > 01_backup.sql
這邊的 –routines 是把 store procedure 匯出(預設 triiger 會匯出)
另外就是 -B
還原至 MySQL :
cat backup.sql | docker exec -i 5b6d /usr/bin/mysql -u root --password=pass.123 test
到這邊我們就可以看到資料成功還原至 MySQL 了.
怕麻煩的朋友甚至可以寫成 .sh 檔案加快協同開發:
mysql-docker-export.sh 內容:
1 | Backup |
這邊的 1ddf 是 MariaDB 的 container id, 5b6d 是 MySQL 的 container id
init.sql 內容:
1 | CREATE USER 'blackie'@'%' IDENTIFIED BY 'pass.123'; |
程式碼實作參考dotnet-mariadb-lab 內的:
資料庫的交易(Transaction)功能,能確保多個 SQL 指令,能夠一起全部執行成功,或是全部不執行,而不會因為一些意外狀況,而只執行部份指令,造成資料異常。
交易功能4個特性 (ACID)
MySQL 常用的兩個資料表類型:MyISAM、InnoDB,MyISAM 不支援交易功能,所以以下使用交易時也是需要使用InnoDB。
詳細的內容可以參考MySQL 交易功能 Transaction 整理這篇詳細的整理
InnoDB 支援全部四種 Isolation Level ,使用者可以用 SET TRANSACTION 語法切換。
InnoDB 預設的 Isolation Level 是 REPEATABLE READ ,而 REPEATABLE READ 的問題就是有可能 Phantom Read
以 MySQL 8.0 Isolation Level 有以下等級分類:
這邊如果要了解Isolation分級可以參考:資料庫交易的 Isolation
MySQL 與 MariaDB 中的 Transaction 與 TransactionScope 雖然都可以達到交易鎖定與一至性交付的目的,但使用地情境卻大不相同.
針對同一個資料庫實體進行交易鎖定時,多半會使用 Transaction 的方式建立單一資料庫連線進行設定.應該所有的 db driver 都有支援.
而針對跨多種資料庫類型或是多個不同連線的情境則會使用 TransactionScope 的方式來幫我們確認分散式交易(distributed transaction)能確實在多個實體內如期按照規劃運行.這邊除了要確認使用的db driver 支援外,也要確認環境設定等配製是正確的,如 MSSQL 就是使用 MSTDC 進行控制.
這邊如果要在 DB 直接進行 SQL 的指令運行 Transaction 如下:
1 | BEGIN; |
結果可以看到上方的查詢有顯示最後新增的資料,但真實進去資料表內查詢則維持原樣,表示交易成功回朔:
接下來的交易實作的範例都已 ADO.NET 為例:
預設的情況, MySqlConnector 需要設定每一個Transaction 內的 MySqlCommand.Transaction 的內容,透過同一個 DB 連線與設定的 Transaction 一至性來達到交易的確認或是回朔.
這篇 Transaction Usage 就提到可以在 Connection 字串後面補上 IgnoreCommandTransaction=true 來確保不會發生錯誤.
官方範例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15using (var connection = new MySqlConnection(...))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT ...";
// *** ADD THIS LINE ***
command.Transaction = transaction;
// otherwise, this will throw System.InvalidOperationException: The transaction associated with this command is not the connection's active transaction.
command.ExecuteScalar();
}
}
實務上,複雜的資料查詢或是資料的新刪修因為牽扯到交易鎖定,大多會在 預存程序(stored procedure) 內做掉,而當我們今天有一個測試的需求如下:
1 | // Arrange |
上面是一個交易的實際寫法,而程式碼內可以發現我們呼叫 AddNewUser 與 GetNewUser 這兩個方法,同在新增資料與取得資料內進行邏輯驗證.最後透過 transaction.Rollback() 的方式回逤資料.
這邊要注意的是由於需求會讀到未確認的交易資料,所以必須設定隔離層級為 IsolationLevel.ReadUncommitted.
1 | using System; |
從上面的程式碼可以發現裡面使用了 C#7.0 的local function 來讓我們的程式碼可以盡量重複使用同一段邏輯.並搭配 optional paramater 我們可以盡可能讓測試程式碼與真實運行的測試碼走過一樣的邏輯確保測試的正確性.
這邊需要注意如果是 ExecuteReaderAsync() 將 DataReader 資料讀出,一定要記得 dispose 或是透過範例的方式正確的透過 using 的預設呼叫 dispose 來避免程式運行時發生 System.InvalidOperationException : This MySqlConnection is already in use 這類的問題!
MySQLConnector 在安裝與設定的指引那邊 Migrating from Connector/NET有提到
目前的更新版本已經全面支援 distributed transaction , 這一舉解決了在 2017 以前官方 client(MySQL Connector/NET) 長久存在的問題.
實務上,TransactionScope 的應用範圍會再異質資料庫牽扯到一筆交易需要多個 Connection 的交易範圍鎖定,測試的需求如下:
1 | // Arrange |
這邊可以看到使用上必須設定 TransactionScopeAsyncFlowOption.Enabled 確保兩個不同 thread 的交易可以被鎖定.另外也需要注意 IsolationLevel.ReadUncommitted 這個設定是否合乎真實的使用情況(一般來說不會使用當前的設定,會在嚴格一點)
而呼叫資料庫的方式則大同小異:
1 | using System; |
這邊需要稍微注意 AutoEnlint 這個屬性預設在 Connection 內如果不特定指定則為true.當設定為true時則會將當前打開連線的交易設為同一個 transaction scope.所以如果要使用 TransactionScope 千萬不要在 Connection 手動補上 AutoEnlint=false 這樣的設定.
而不管用 Transaction 或是 TransactionScope 都要注意關閉相關資源避免 Lock 發生,讓後續相關查詢與異動失敗:
]]>常使用 Rider 的朋友會發現,每次開出的新專案視窗的版面配置不會跟著上一個設定走,必須每次都調整,有時十分不方便.這邊方享如何快速的統一呈現的方式.
Rider 有幾種環境設定與配置可以跨專案或是跨電腦的與他人共享,這邊分為三個設定:
如果你是要分享一些針對 Theme 的設定或是針對 IDE 的呈現設定,則可透過 Intellij IDEA Settings 協助你與分享或是引入至當前環境
可以從上方選項分類的 File 內看到相關匯入匯出設定
產生的設定檔為一個 .jar 副檔名類型的檔案,使用時就是直接匯入
關於此設定可以看 Exporting and Importing Settings 了解更多細節
如果你是要分享一些 Code Snippet 或是針對 IDE 的開發設定,則可透過 Rider Settings 協助你與分享或是引入至當前環境
可分為兩種:
directory-based settings format:
這與 IntelliJ 平台內的前端開發有關,如 CSS, HTML, JS 等 Editor 內配置與設定.
Layer-based settings format:
使用與 Resharper 相同的 layer-based, 這個設定也可以匯入或匯出 ReSharper 內做使用
在偏好設定處的設定可以根據圖示分辨該設定屬於哪一種設定:
也可以從偏好設定的下方新增或調整當前的設定:
更多細節可以至官方看更多詳細說明 Rider Settings
最後是本篇的主要分享,如果你只是要簡單的想要跨專案的在本機有一致的 Window Laout,則可透過上方 Window 分類內的 Default Layout 功能達到
使用上只需儲存當前調整完的視窗版面,如我將 NuGet 與 Terminal 從原本的下方呈現更改至右方,並按下 Store Current Layout as Default
接著在你原先不符合預期的專案內按下上方 Window > Reset to Default Layout 即可統一呈現了
]]>延續前一篇的Basic MariaDB/MySQL CRUD with .NET Standard
關聯式資料庫依定會遇到 Stored Procedure 的使用,一個 SP 的組成為下:
而在 MySQL/MariaDB 把 Stored Procedures 與 Stored Functions 合稱為 Stored Routines:
Stored Procedures
官方解釋為:
`Stored Procedures Routine invoked with a CALL statement.
是一個可預先宣告的 SQL 語句,可透過 CALL 來呼叫.所以 Stored Procedures 可單獨做使用。
Stored Functions
官方解釋為:
`Stored Functions Defined Functions for use with SQL Statements.
是一個預先定義好的函示,可在任一段 SQL 語句中呼叫使用.所以 Stored Functions 必須依賴在有 Stored Procedures 的情境下做使用(非硬規定)
國外知名論壇也有討論兩者的差異與整理如下:
而如果要撰寫一個 SP 包含資料回傳的話有 output 與 return value 兩種寫法
這邊如果要定義一個 SP 的可以透過下面的範例格式在資料庫實體執行:
1 | DELIMITER // |
以上寫法的第一行 DELIMITER // 主要是將換行字元從預設的 ; 換為 // 避免在建立 SP 產生錯誤,詳細內容可以參考只談MySQL (第16天) Stored Procedure及Function。而最後一行同理就是將其換行字元換為原先的 ;
而上述語法可以看到我們用 OUT 當作外部宣告的參數傳入後在運行完整個查詢後將總數填入此 param1 內,所以今天我們在 MariaDB/MySQL 的 console 或是工具呼叫取用時反而要用下方語法:
CALL QUERY_USERS_COUNT(@a);SELECT @a;
這邊以 Jetbrains 的跨平台資料庫 IDE - datagrip 為例做操作:
而對應的 C# 程式碼為:
1 | public async Task<int> GetUserCountBySPWithOutputValue() |
而如果將上述語法改為 return value 則為:
1 | DELIMITER // |
而這邊呼叫也相對簡單一點:
CALL QUERY_USERS_COUNT_WITH_RETURNVALUE();
而對應的 C# 程式碼為:
1 | public async Task<int> GetUserCountBySPWithReturnValue() |
而完整的範例與測試可以參考:dotnet-mariadb-lab
最近開始學習一些非微軟且 Open Source的技術,在 RDBMS 的選項中選擇使用 MariaDB.今天就分享如何讓 .NET 也能基本的使用CRUD.
MariaDB 資料庫管理系統是MySQL的一個分支,主要由開源社群在維護,採用 GPL 授權授權。MariaDB 是由MySQL的創始人 Ulf Michael Widenius 主導開發並以他女兒的名字為該專案命名.
MariaDB 的特色是 MariaDB 的 API 和協定相容 MySQL,但又擴充了一些功能,以支援原生的非阻塞操作和進度報告。這也讓當前使用MySQL的連結器、程式庫和應用程式也將可以在 MariaDB 正常運作.
整個 MariaDB 的架構可以參考下圖:
更深入的介紹可以參考MariaDB: in-depth (hands on training in Seoul)
由上面簡單的介紹可以知道 MariaDB 基本上跟 MySQL 是相同但跟先進,且所有工具都能直接沿用的.
所以這邊直接切入本篇重點使用 MySqlConnector 來協助 .NET 操作 MariaDB 的資料.
MySqlConnector 不是 MySQL 官方推出的套件,但號稱比官方效能更好且完美的支援常見的 ORM 框架如:Dapper, NReco.Data, Paradigm ORM, ServiceStack.OrmLite 與 SimpleStack.Orm 等。
而最棒的是他有實作非同步的介面讓開發更為方便。
這邊的範例使用 Docker 來運行 MariaDB, 操作上只有兩步驟:
拉取 MariaDB image
docker pull mariadb
運行一個 Instance 並設定密碼與 port 對外
docker run -p 3306:3306 --name lab-mariadb -e MYSQL_ROOT_PASSWORD=pass.123 -d mariadb
接著開一個 .NET Standard 專案後從 nuget 加入 MySqlConnector
dotnet add package MySqlConnector
接著我們簡單地透過以下的樣式就可以建立第一個 Read 的操作:
1 | public class UserRepository : IUserRepository |
從上面可以看到步驟依序為:
以上就是一個基本的流程,所以同理補上新增的範例如下:
1 | public async Task CreateUser(UserDto inputObj) |
這邊參考了官方的建議使用在每一個操作後就透過 conn 外面包裹的 using 架構,直接 Dispose 時關閉 DB 連線,避免有漏關的狀況發生。
而上面的程式碼寫法與相關注意事項可以參考以下三篇文章的說明:
而完整的範例與測試可以參考:dotnet-mariadb-lab
Rider 在 2018.2 版本推出了兩個新功能 Live templates 與 File templates 的流程改進,透過這兩個功能我們可以
處理重複內容、建立程式碼樣板和各項語言撰寫前的宣告或是呈現的預設樣式,原文介紹:Live templates and file templates in Rider 2018.2,這邊就簡單分享一下如何使用這兩個功能.
這個功能可以很快速的幫我們透過以建立的程式碼樣板帶入當前程式區塊,類似code snipet 的效果,但更為強大的是可以動態的決定一些帶入參數與語言結構.
這邊我們很快的輸入在建立一份 C# 函示如下:
1 | public void DoTest() |
當我們建立以上內容後可以透過在下一行開始輸入 foreach 等已建立的預設 live templates 關鍵字建立樣板:
接著可以按下 tab 或是 enter 引入這個樣板.
而當樣板被引入後,即可輸入前面宣告的變數 datas. 這邊只要輸入前幾個想選擇的變數前面的名稱,當選項已經反白鎖定了就可以按下 tab 自動帶入並移動到前方再度詢問是否要變更設定, 此時按下 tab 則會產生預設的迴圈內變數,直接按下 tab 就可以完成整個迴圈設定:
透過以上方法我們能更快速的實作常用的程式碼區塊.
而預設建立的 live templates 樣板可以從 Preferences > Editor | Live Templates 這邊看到:
而從設定中我們也可以看到有一個選項是 Generate, Surround or Both, 上面的範例就是一個 Generate 的使用方式,而 Surround 的用法則是在你原本想要再迴圈內運行的區段按下快捷鍵 Code | Surround With… (Windows 為 Ctrl+E,U 而 macOS 為 ⌘ ⌥ T))
這邊就可以這邊就可以如下叫出快捷並選擇要包裹的類型為哪一個樣板:
整套原文的做法可以參考這邊示範:
而不同於 live templates 是在已經建立的程式檔案中方便插入常見的程式碼樣式或區塊,File Templates 更加用於建立一份已知的預設檔案樣板,這邊已先前介紹過的 Singleton Pattern Implementation in C# 為例子, 建立一個 Singleton 的檔案大概如下:
1 | using System; |
從上面的檔案我們就可以建立一個 Singleton Helper 的樣版,以便之後快速建立向同框架或程式碼結構的其他檔案.
首先到 Preferences > Editor | File Templates ,我們就可以建立一個新的樣版並給她以下設定:
其中畫面類似程式碼區塊的就是我們剛剛擷取的樣版:
using System;$HEADER$namespace $NAMESPACE${public sealed class $CLASS$ { private static readonly Lazy<$CLASS$> lazy = new Lazy<$CLASS$>(() => new $CLASS$()); public static $CLASS$ Instance { get { return lazy.Value; } } private $CLASS$(){$END$}}}
將一些需要帶入的專案變數用特殊的寫法包起來並透額外透過下面的步驟逐一設定(這邊可以參考已經建立好的預設樣板各變數是取用什麼對應):
到這邊確定完後記得要點選右邊的閃電標記並將他移入當前使用才可生效:
最後只要在剛剛樣板可以生效的轉案類型,在新增檔案時可以從 context 看到有多一個剛剛建立的樣板選項:
接下來只要輸入你想要的檔案名稱,他就會以該名稱建立一個同名的 Singleton 模式的類別供使用.
]]>昨天將手上 macbook 升級至最新的 macOS:Mojave 後,透過 homebrew 進行套件更新時就會發生 missing xcrun 的問題: invalid active developer path,導致 git 指令不能正常執行成功.詳細的錯誤如下:
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
查了一下相關資料,有可能發師的原因是 xcode 的套件位置移動或是位置不正確了,透過以下指令可以重新設定相關設定:
xcode-select --install
接著會跳出視窗一步步按下同意與確認即可.
而當我們再次使用 homebrew 進行更新後即可以看到正確的回應
]]>在 OOP 的開發裡面,繼承是一定會使用到的手法,而當我們繼承抽象類別與介面要一一找出必要的實作時難免有點麻煩.
以往我們最常用的快捷鍵莫過於直接輸入 ctor 並按下 tab 後幫我們產生類別建構子:
而現代的 IDE 都已經內建快捷鍵協助快速實作方法,免去我們打字的困擾.
在 Rider 當中我們可以透過滑鼠右鍵的 Generate 或是 ⌘+N > select Implement method 的方式:
當然我們也可以直接按下快捷鍵 ⌃+I 省去上面的步驟,直接選擇我們要實作的方法來幫我們自動產生程式碼:
完成後,產生的程式碼暫時透過丟出 System.NotImplementedException() 的方式來完成實作:
而 VS 其實很早就支援這樣的快捷鍵,一樣可以透過滑鼠右鍵選擇 Implement Abstract Metohd 或是直接按下 Ctrl+.,呼叫出以下畫面:
Docker 是近年必備的開發技能之一,有不少企業也將此技術放置正式環境上作部署與運作.如果今天有需要從非官方的 docker registry(也就是不是從 hub, https//hub.docker.com/) 就會需要額外進行 login 並在 pull 時搭配指定的 registry 位置才可以成功取得,以下用 Redhat regisry 為例 :
docker login registry.access.redhat.com
登入成功後即可拉取映像檔案,這邊以.NET Core 2.0 Runtime on RHEL為例:
docker pull registry.access.redhat.com/dotnet/dotnet-20-runtime-rhel7
如此便可取得非官網的 registry 檔案.但每次都要打上前綴的 registry url 實在很煩人,這時我們就拿出上次介紹的 Portainer - Simple and useful management UI for Docker
運行 Portainer 後可以在 Settings> Registries 找到新增 Registry 的地方:
當設定好後,可以到 Images 選擇拉取一個新的 image ,這邊選擇剛剛的註冊的 registry 並輸入指定的 image 名稱即可:
是不是很簡單!趕快改用 Portainer 做你的容器管理工具吧
]]>早先在 Visaul Studio Code Quick Add gitingore File 有介紹過 vscode 的 extension 去協助我們針對語言或專案特性快速產生 .gitignore,省去繁雜的查詢與貼上.
由於最近使用 rider 作為主要 C# 開發工具,所以找了上面的擴充套件 - Plugins - .ignore.
該套件目前不只支援 Rider ,也支援以下 IDE:
如果有使用 jetbrains 家族的其他產品也非常推薦下載.
打開 Rider 先至 preferences > Plugins :
選擇下方的 Browse Repositories 或是手動選擇加入下載的套件. 這邊以 Browse Repositories 為例,進入後上方輸入你要查詢的套件並按下安裝即可:
安裝好後會在你專案(.csproject)內透過右鍵或是新增檔案快速加入 .gitignore.
但如果在方案(.sln)則反灰無法使用:
如果需要在方案建立則可以透過以下方法
而 gitignore.io 其實近年也出了自己的 CLI(Command Line Tool) - gi ,可以讓使用在 bash, zsh, fish 或是 windows cmd 與 powershell 下都能快速的產生 .gitignore 檔案.
安裝上就參考你所需要安裝的環境執行指令,這邊以 macOS 的 zsh 為例,到 zsh 貼上下面指令即可:
echo "function gi() { curl -L -s https://www.gitignore.io/api/\$@ ;}" >> ~/.zshrc && source ~/.zshrc
安裝好後可以透過以下指令顯示所有 .gitignore 樣板:
gi list
透過以下指令即可將指定樣板加入選定的位置:
git visualstudio >> .gitignore
這邊可以將一個以上的樣板同時加入
上方指令運作完後透過顯示全部檔案即可看到檔案成功加入
接著,可以回到 Rider 透過顯示所有檔案看到剛剛被加入的 .gitignore
這邊不管是透過第一個方法還是第二個方法都會幫你加入一樣的 .gitignore, 如果是針對 rider 要客製化 git ignore 檔案的話只需加入下面至 .gitignore:
# JetBrains Rider.idea/*.sln.iml
]]>前陣子再一個經常操作的專案上做了 git commit 後,從當前的 HEAD 切換到 master 分支,部分交付的 commits 在 HEAD 操作的就全部遺失了,在 git cli 或 sourcetree 上都無法透過指令顯示不見的 HEAD 分支.
後來想到 Git commit 的特性,就透過 git reflog 指令然觀看全部 git 的操作記錄,裡面詳細記載你曾經下過的 git 指令:
git reflog
這邊找到了移動到 master 分支前的一個 commit 的雜湊值(hash)為 596f379….(後面省略).
這邊可以馬上透過建立 tag 的指令將當前消失的 commits 顯示出來:
git tag emergency 596f379
當完成後回到 SouceTree 就可以看到結果如下:
這邊說明一下 HEAD 分支與那些 commits 原先之所以在切換到 master 會完全消失的原因在於:
詳細內容可以參考 Git-內部原理-Git-References.
當 HEAD 往新的 master branch 移動時,先前的 commit 因為沒有任何所屬的 branch, 所以除非給予一個 tag ,不然就真的消失了(其實是看不到而已)。而透過 git reflog 指令可以顯示所有操作的內容,從此指令我們就可以抓到消失的 sha1 ,並透過 git tag 指令給予一個 label 將所有相關 commits 顯示出來。
一個很久沒遇到的問題,這邊筆記一下以免下次又找半天.