[實作筆記] ASP.Net Core Config 的隨筆

提要

  • DI Configuration
  • 組態的層次
  • Options Pattern

DI Configuration

dotnet webapi 專案預設會注入 IConfiguration ,
在專案中的程式建構子放入 IConfiguration 就可以直接取得組態設定

1
2
3
4
5
6
7
8
9
10
11
12
private readonly IConfiguration _config;

public WeatherForecastController(IConfiguration config)
{
_config = config;
}

[HttpGet(Name = "GetWeatherForecast")]
public string Get()
{
return _config["ASPNETCORE_ENVIRONMENT"]!;
}

你可以將組態設定在 appsettings.json 之中
或是透過環境變數,使用環境變數有幾種作法

  • 設定在機器上
  • 設定在 Cloud Service 所提供的組態管理工具之中
  • 使用 launchSettings.json

在開發環境我會使用 launchSettings 好處是可以不需要真的在開發機的環境中設定環境變數,
對同時擁有多個專案的開發者而言,可以有效隔離不同的專案變數,避免彼此影響污染

組態的層次

為了方便管理,我們可以透過組態的層次來加以分門別類。
比如說:

1
2
3
4
5
6
7
8
"Level": {
"First": {
"Key": "1st Value"
},
"Second": {
"Key": "2nd Value"
}
},

一般的 JSON 或 XML 要作到這樣的層次結構都相當的簡單,
而要取用組態的方式如下:

1
var key = _config["Level:First:Key"];

不過我們要注意環境變數並無法使用:字符,也沒有 JSON 或是 XML 所能提供的結構,
這裡要注意可以用 __(雙底線(Double underscore))來建立層次。

1
2
$Level__First__Key
$Level__Second__Key

Options Pattern

Options Pattern 可以提供一種更物件導向的組態配置解決方案,
你只需要兩個步驟,即可實作 Options Pattern

  1. 建立 Option 物件
    我在這裡提供二種不同的方法,後面會再說明

    1
    2
    3
    4
    5
    6
    //方法一 Dictionary
    public class LevelOptions
    {
    public const string Level = "Level";

    public Dictionary<string,string> First { get; set; }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //方法二 Object
    public class LevelOption
    {
    public const string Level = "Level";

    public FirstOptions First { get; set; }
    }

    public class FirstOptions
    {
    public string Key { get; set; } = string.Empty;
    }
  2. 註冊 Option 物件

1
builder.Services.Configure<LevelOptions>(LevelOption.Level)

最後是讀取組態,在方法一(使用 Dictionary)的方法讀取

1
var key = _options.Value.First["Key"];

或是方法二(使用 Object)的方法讀取

1
var key = _options.Value.First.Key;

兩種方法都可以,物件化的方法需要建立多個物件,
但是在取用組態的過程就不會有 hard coding string;
反之 Dictionary 的方法可以少寫很多物件。

對我而言我比較偏向使用 Object 的方法去處理,只要適當的分類,
物件多並不是問題,而且通常一次不會增加太多的組態,可以迭代增量。

順代一提,如果組態檔層次太多,會是另一個困擾 ? 我的建議是,如果組態的深度到達 3 時,
需要考慮這是不是一個壞味道,是不是過度優化了 ?
實務上來說,我會避免建立超過 3 層,也很難會有一定要超過 3 層的情境,
如果你有什麼情境上需要超過 3 層,可以留言給我,我們可以一起討論。

在 Program.cs 使用

如果在 Program.cs 的注入階段,如果要取得組態,
可以用
(new ConfigurationManager()).GetSection("First")["Key"]
或是 Environment.GetEnvironmentVariable("Level__First__Key"); 等方法取得組態。
但是我更推薦 ServiceProvider 的 GetRequiredService 取得 Options

1
2
3
4
5
6
7
8
9
10
//注入在前
builder.Services.Configure<LevelOption>(builder.Configuration.GetSection(LevelOption.Level));
builder.Services.AddHttpClient("OtherService", (provider, client) =>
{
//透過 provider 取得 config
var o = provider.GetRequiredService<IOptions<LevelOptions>>();
client.BaseAddress = new Uri($"{o.Value.Url}/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", o.Value.Token);
});

這樣就可以用一致的方法取得組態了。

參考

(fin)

[實作筆記] Hexo 更新 Google Analytics 4

前情提要

幾天前收到來自 Google Analytics 的這樣一封信

1
2
3
4
5
Google Analytics (分析) 4 即將取代通用 Analytics (分析)

我們即將以新一代的成效評估解決方案 Google Analytics (分析) 4,
取代通用 Analytics (分析)。從 2023 年 7 月 1 日起,通用 Analytics (分析) 資源將停止處理新的命中資料;
如果通用 Analytics (分析) 仍是您常用的工具,建議您採行相應措施,準備好改用 Google Analytics (分析) 4

實作步驟

參考以前我寫的這篇–如何讓 Google Analytics 追踪你的 Hexo Blog
我們知道在 hexo 的佈景主題 landscape 有提供舊版 GA 的設定,
我們只需要修改 theme/layout/_partial/google-analytics.ejs 即可
程式參考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<% if (theme.google_analytics){ %>
<!-- Google Analytics -->
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= theme.google_analytics %>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', '<%= theme.google_analytics %>');
</script>
<!-- End Google Analytics -->
<% } %>

由於通用 GA 在 2023 年後就不再支援,所以我就不特地處理相容,直接切換到 GA4

參考

(fin)

[實作筆記] 簡單的方法 Mock TypeScript Interface

前情提要

我目前在實作一個 React 的專案,並結合 Storybook 來實現 CDD(component driven development),
我很喜歡 CDD 的概念,這裡解決 TDD 對 View 難以趨動開發的痛點,
但是又維持了 Unit Test 的基本精神,進一步結合 Chromatic 可以提供視覺化的檢核頁面,
作為與 PO/PM/UX/UI/DESIGNER 之間的溝通工具非常好用。

問題

我作了一個簡單的 Header 組件,
這通常是網站或 APP 上方的區塊,通常會提供一些額外的資訊或功能,
例如: 登入/登出,而這裡我使用 Firebase 作為實作登入/登出的功能,
作為主要的邏輯判斷,firebase 的 Auth 物件會被傳入

1
2
3
Header = (props: { sighOut: () => void; auth?: Auth }) => {
//...
};

這裡我們先不討論 Auth 的邏輯是否應該與 Header 相依(嗯,這裡看起來是個壞味道),
而是在我們的 Storybook 撰寫測試案例時,應該如何 mock Auth 物件。
我的程式都是用 TypeScript 與 tsx 寫的,
所以如果不符合型別,IDE 會直接報錯,程式跟本無法執行

1
2
3
4
5
6
7
8
const Template: ComponentStory<typeof Header> = () => (
<Header
sighOut={() => {
alert("mocked Signout");
}}
auth={mockAuth}
/>
);

試過的方法

  • jest Mock
  • ts-mockito
  • ts-auto-mock

以上的方法有的成功,有的失敗,最大的問題是在配置與撰寫其實很麻煩,
甚至會有同事說為什麼要用 TypeScript 自找麻煩呢 ?

逐一 mock 所有 interface 的屬性,對於一個像 Auth 這樣複雜的介面實作上相當麻煩,
而且大多 mock 的屬性都用不到

我的想法是,應該是我對 TypeScript 與其生態圈不夠了解,寫起來才會覺得綁手綁腳的,
不能推缷給程式語言,同時 TypeScript 強型別的好處,其實也幫了我不少。
仔細研究後,我找到一個簡單方法如下:

解法

不需要 mock Library , 不需要麻煩的設定或是第三方套件,
最簡單的寫法 as 就可以將物件 mock 成為你想要的 Interface

1
2
3
const mockAuth = {
currentUser: { email: "[email protected]" },
} as Auth;

這樣的寫法除了可以只 mock 必要的屬性外,
如果你物件中包含了不屬於介面應有的屬性,
TypeScript 將會提出警告,非常好用。

(fin)

[實作筆記] 升級現有基於 TypeScript 的 React 17 專案到 React 18

前情提要

升級 React 在官方的文件之中,似乎是一件極為簡單的事,
但是如果你的專案是基於 TypeScript 開發,
在前幾周應該無法順利升級,主要的原因是相對應的 @types 套件並未同時更新,
幸運的是,現在看到文章的你,已經可以順利更新了,請參考以下的步驟。

記錄

安裝 react、react-dom、@types/react、@types/react-dom

1
npm i react@18 react-dom@18 @types/react@18 @types/react-dom@18

更改 ReactDOM.render 的寫法

原本的寫法

1
2
3
4
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<App />, document.getElementById("root"));

後來的寫法

1
2
3
4
5
6
import React from "react";
import ReactDOM from "react-dom/client";

const ele = document.getElementById("root") as Element;
const root = ReactDOM.createRoot(ele);
root.render(<App />);

特別注意要處理 Element 的型別

參考

(fin)

[踩雷筆記] Brave 瀏覽器與本機開發 http

前情提要

Brave 是一款主打安全、注重隱私的瀏覽器,雖然目前仍相當小眾,
但在開發者中有一定使用者。

https 通訊協定現在已經是一個 Web 服務、網站、api 的基本安全配備,但是對於開發者來說,
在本地開發常常僅使用 http 通訊協定就足以作功能性的驗証。

情境

使用 dotnet core 建立了一個 web api 的服務,預設的啟動站台 end point 為 http://localhost:5000
而 Brave 作為預設瀏覽器開啟後,會看到 403 的錯誤頁面。

1
2
3
找不到 localhost
網頁找不到此網址的網頁:http://localhost:5000/
HTTP ERROR 404

解決方法

正常的思路,應該是讓本地開發環境建立起 https 的連線,這裡又會有相關的憑証問題要處理,
應該有蠻多不同的作法,改天我再寫一篇相關的介紹。

短解很簡單,換個瀏覽器就可以了,
如果堅持要用 Brave ,可以至設定 > 防護 > 關閉「升級連線至 HTTPS」的開關即可。
順代一提,當我使用 hexo、React 等工具建立靜態網站時不會有這個問題。

參考

(fin)

[實作筆記] 靜態網站部署整合 GCP (二) --- Load Balancing & Cloud Storage

前請提要

去年有記錄一篇有關 GCP 部署靜態網站時遇到情境與問題,
不過比較偏像流水帳,不同的部署策略都在同一篇文章,另外有一些問題也得到解決,
除了更新原有文章外,這篇的目的是為了將 Load Balancing 與 Cloud Storage 抽出,
並加以潤飾。

概要

  • 在 Cloud Storage 建立一個 Bucket
  • 在 Network Service 建立一個 Load Balancing
  • 設定 Load Balancing 的 Backend 綁定到 Bucket
  • 作為網站,需要允許所有人讀取 Bucket 的內容

必要條件

GCP 平台的帳戶與足夠的權限,並且已建立專案

在 Cloud Storage 建立一個 Bucket

第一步前往Cloud Storage
點擊 +CREATE BUCKET,
在 Choose where to store your data 的區塊,
Location type 有三種

  • multi-region
  • dual-region
  • region

這是地理位置與高可用性的相關設定,越後面的設定成本越便宜,但是可用性也越低。
即使如此 google 仍保証了 SLA: 99.95% 的高可用性。

接下來是 Choose a default storage class for your data 的區塊,有

  • Standard
  • Nearline
  • Coldline
  • Archive

等四種不同的設定,
與檔案的使用頻率有關,對於網站來說建議使用 Standard 。
收費可以參考下表

Standard Nearline Coldline Archive
Storage(per GB-Month) $0.026 $0.01 $0.007 $0.004
retrieval(per GB-Month) Free $0.01 $0.02 $0.05
Class A Operations(per 1000ops) $0.005 $0.01 $0.01 $0.05
Class B Operations(per 1000 ops) $0.0004 $0.001 $0.005 $0.05
SLA 99.95% 99.9% 99.9% 99.9%

接下來是 Choose how to control access to objects 的設定,
不要勾選 Enforce public access prevention on this bucket,
Access control 選擇 Uniform , 這裡的設定是為了避免從 internet 存取 bucket 的資料,
但是我們的目的是放置靜態網站的資料,所以不需設定。

再來是 Choose how to protect object data
這是保護資料的策略,有版本(versioning)與備份(retention)兩種策略,
我們不需要所以選擇 None

按下 Create 以建立 Bucket,
接下來為了讓 web 存取我們選擇 more action(3 個點的 Icon ) > Edit Access
New Principals > 選擇 allUsers > Storage Object Viewer.
接下來可以上傳你的靜態網站的資源了,這裡我們多作一個設定,通常我們要指定網站的首頁為何,
約定成俗是 index.html,一樣 more action(3 個點的 Icon ) > edit website configuration
將 Index (main) page suffix 設定為 index.html(記得 bucket 裡要有這個檔)

Load Balancing 相關設定

接下來設定 Load Balancing,
這只需要設定一次,在目前的實作未涉及 VPC、CDN、Path Rules 等相關議題,
實務上需要的話,請再加上去考量。

  1. GCP > Network Service > Load Balancing
  2. Create Load Balancer
  3. Backend configuration > Create A Backend Bucket
    • 自已取一個 Backend Bucket name
    • Cloud Storage Bucket > Browse > 選取之前所建立的 Bucket
    • 不勾選 Enable Cloud CDN, 相關的設定與應用程式的應用有關, 較為複雜之後再進行處理
  4. Host and path rules > Simple host and path rule
  5. Frontend IP and PORT > Add Frontend IP And Port
    • Protocol > HTTP (正常應使用 HTTPS, 這次為了求快而未作相關設定)
    • Network Service Tier > Premium

可以參考 GCP 的教學與我們實作上的細微差異

  • Create a bucket. → 手動建立只需要處理一次
  • Upload and share your site’s files. → CI 執行
  • Set up a load balancer and SSL certificate. → 手動建立只需要處理一次
  • Connect your load balancer to your bucket. → 手動建立只需要處理一次
  • Point your domain to your load balancer using an A record. → 未處理
  • Test the website.

CI/CD 的相關設定 (已過時)

參考以下部份的 .gitlab-ci.yml 檔
非常簡單,只需要 gsutil rsync -R 將前一個 job 建置的檔案推到 Cloud Storage 即可

1
2
3
4
5
6
7
8
9
10
deploy-job: # This job runs in the deploy stage.
stage: deploy # It only runs when *both* jobs in the test stage complete successfully.
image: google/cloud-sdk
needs:
- job: build-job
artifacts: true
script:
# - gcloud auth list # Show the ACTIVE ACCOUNT *
- gsutil rsync -R build gs://your_bucket_name
- echo "Application successfully deployed."

其它(已過時)

這裡仍有一個未知的設定,gsutil 命令會需要權限才能對 Cloud Storage 寫入,
如何讓 Gitlab-Runner 底下的 Container 擁有指定的 GCP 權限呢 ?
試著下 gcloud auth list 會發現確實有一個可工作的 Account 所以一定有相關的設定要處理,
因為沒有實作到,故不作記錄,但未來再有機會不要忘了這一段的工程。

大部份的工作只需要設定一次,未來只需要 CI 將新版的靜態網站上傳到 Cloud Storage 網站就會更新。

參考

(fin)

[生活筆記] 2022 貓劇感想

第一次看到貓劇是在高中音樂課上,老師所播放了 DVD 的片段,
具體的內容也記不清了——當時的老師常常放影片上課,
比如說用廚具演奏的樂團、貓劇等,——
當初只覺得特別,甚至有點詭異,並沒有什麼感觸。

大學畢業後,等待當兵的時間,不知道在什麼時間點,我再次接觸了貓劇。
這次就陷入狂熱(FEVER),一開始是在網路上找片段看,後來也買了 DVD,
整個部片應該看了不下 30 次,甚至是可以聽著音樂睡覺。
我最喜歡的是看每隻貓咪的互動與細節。從那個時候開始,我就希望可以前往看貓劇的演出。

我已經不確定當時有沒有確認過停演資訊,但是我知道倫敦是貓的首演場地,
所以我心中一直想去倫敦參觀(更多的部份是阿森納)
而目前知道倫敦已經沒有演出了,順代一提,歌詞有偷婊熱刺的部份 XDD

1
2
3
4
Grizabella, The Glamour Cat

She haunted many a low resort
Near the grimy road of Tottenham Court

2018 年我開始學習 Lindy Dance 與聽爵士樂,2020 在課堂上認識了一些朋友有在看音樂劇,
聊天過後,再次激起心中的火花。2021 雖然歷經疫情,但是台灣國內的狀況一直相對穩定,
所以開售當天馬上買了票,在朋友的推薦下,大手筆直接買了衛武營 5800 的票,
結果疫情爆發,就拖到了 2022 年。
雖然票價不便宜,但是這次的體驗真的值得。

好的初體驗,第一次看音樂劇就是史上有名的劇,位置在前排中間的位置;
先說說硬體設施,前排是看不到字幕的,不過貓不是一部需要字幕的戲劇,
特別是我以前看過無數次的前提下,除了 Growltiger 的部份沒有看過外,
其它的部份我都相當的熟悉,加上文本本身是詩集的原因,所以其實並不需要太過依賴字幕,
如果英聽夠好的話更是不需要。

第一排的好處是可以看到細節,這點非常適合貓劇,比起劇情與主要出場貓咪的表演來說,
更重要的是散落在背景的貓咪表情、肢體其實正在無時無刻演出,有時他們的互動會讓我誤以為是演員在聊天,
但是隨著演員的動作,才會發現即時聚光燈不在他們身上,他們也仍然在演出,只要在舞台上就是戲的一部份。
而這個距離也可以看到布景的細節
聲音部份是真的可以聽到人聲,不是透過音響的聲音,可以感覺到人聲的媚力,
這次我的位置聽 Cassandra 聲音真得超清楚(聽說麥克風是藏在他們的頭上,
但是他們會互相撥頭時也不會有聲音,所以收音對我來說還是個迷)

  • 會被演員當 Target Point Out ,被點到時真得很有觸電感
  • 終場 Rum Tum Tugger 會超近

但是也是有缺點的部份,首先是視覺的角度較低,在

  • 衛武營第一排有時會被燈箱(或是風扇、提詞機)或貓檔住
  • 當貓咪圍成圈的時候也會被檔住
  • 側面進場的貓咪要轉頭才知道,沒有機會互動

下次看建議的位子是可以平/俯視舞台的地方,靠走道有機會互動

這次補上更多細節是 DVD 沒有的

  • 三大母貓的舞蹈很多也很好看(dvd 看不到完整的)
    • Demeter >> Munkustrap
    • Bombalurina >> Rum Tum Tugger
    • Jellylorum >> Gus(Growltiger >> Griddlebone)
  • JennyAnyDots 有一大段的 TAP 表演(有點像火焰之舞)在 DVD 是刪減版
  • 群舞時有很多三層次的編排在 DVD 看不出來,太近也會看不清楚
  • Mungojerrie And Rumpelteazer 的排舞有點像馬戲團的表演,不太確定舞蹈風格
  • Growltiger >> Griddlebone 的表演像義大利歌劇
  • Tantomile & Coricopat 通常會率先作出反應
  • Victoria 在 Old Deuteronomy 歌詞中 Victoria’s accession 被提到時會被 Munkustrap Cue 一下這點很可愛
    • 常在 C 位
    • solo 的部份我覺得更像體操或芭蕾
    • 舞會 spotlight 的部份搭配的是 Plato/George/Admetus(名字不一定)
  • Jemima 才是第一個用唱歌的方式回應 Grizabella 的小貓,一直記錯成 Victoria (Victoria 是用肢體接觸),初演是 Sarah Brightman 飾演的角色,這次有一段用中文演唱(但是我其實希望他能唱原文就好…中文讓人出戲)
  • Mistoffelees 是芭蕾的表演,特別是連續轉圈(The Conjuring Turn)
  • 我以為演員會讓我認不出來,但是化裝真的很厲害,就像是有臉譜一樣,跟 DVD 版本沒有什麼差別
  • Grizabella 被趕走的時候讓我想到取消文化與 Lindy Dancer Max 的事件
  • 2019 電影版是邪典加悲劇,我反而希望可以買到 DVD 版本的數位檔
  • 2022 年在台灣有幫 Old Deuteronomy 脫口罩(好像是 Alonzo), 有一種現實的可愛無奈

參考

-https://leticiaschic.com/cats-musical-actor-part1/

(fin)

[實作筆記] SonarCloud with .NET 6.0.x

前情提要

承上篇,在升級了 .NET 6.0.x 後發現,
SonarCloud 的測試含概率不知何時變成 0 ,
相對應的 Code Smell 也沒有作檢查, 加上原本的 github/workflow 並沒特別分門別類,
所以真正的錯誤原因已經不可考, 修復 SonarCloud 的過程中特別撰文記錄.

實作

第一件事是在升級 .NET 6.0 後, 試著在本地環境上執行檢查並上傳到 SonarCloud.

前置條件

本機的執行步驟

  • dotnet sonarscanner begin

    1
    2
    3
    4
    5
    6
    7
    dotnet sonarscanner begin \
    /o:"marsen-github" \
    /k:"Marsen.NetCore.Dojo" \
    /n:"Marsen.NetCore.Dojo" \
    /d:sonar.host.url="https://sonarcloud.io" \
    /d:sonar.cs.opencover.reportsPaths="./test/_/TestResults/_/coverage.opencover.xml" \
    /d:sonar.login="****"

    參數說明
    /o: Organization Key
    /k: Project Key
    /n: Project Name
    /d:sonar.host.url: sonar server url
    /d:sonar.cs.opencover.reportsPaths: OpenCover coverage report
    /d:sonar.login: 登入 sonar server 的 token

  • dotnet build

    1
    dotnet build Marsen.NetCore.Dojo.sln
  • dotnet test

    1
    2
    3
    dotnet test Marsen.NetCore.Dojo.sln \
    --logger trx --collect:"XPlat Code Coverage" \
    -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
  • dotnet sonarscanner end

    1
    dotnet-sonarscanner end /d:sonar.login="****"

Github Workflow 的設定

目前並沒有官方的解決方案 ,
我們使用 SonarScanner for .NET 來執行 workflow ,
這個專案十分簡潔,可以快速看一下內容,了解到怎麼實作一個 workflow ,
因為非官方解決方案,所以我 fork 這個方案避免意外發生.

而在 workflow 中設定的也是使用 fork 的方案
設定參考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- name: SonarCloud Scan
uses: marsen/[email protected]
with:
# The key of the SonarQube project
sonarProjectKey: Marsen.NetCore.Dojo
# The name of the SonarQube project
sonarProjectName: Marsen.NetCore.Dojo
# The name of the SonarQube Organization
sonarOrganization: marsen-github
# Optional extra command arguments the the SonarScanner 'begin' command
sonarBeginArguments: /d:sonar.cs.opencover.reportsPaths="./test/*/TestResults/*/coverage.opencover.xml"
# Optional. Set to 1 or true to not run 'dotnet test' command
# dotnetDisableTests: true
dotnetTestArguments: Marsen.NetCore.Dojo.Integration.Test.sln --logger trx -p:CoverletOutputFormat="opencover" --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
dotnetBuildArguments: Marsen.NetCore.Dojo.sln

其它

注意到 opencover 的開發者已經不再維護(2021 年 6 月),並建議使用 AltCover.
但是 sonarcloud 並未支援 altcover 的報表, 這裡有相關的 issue 追蹤中.

參考

(fin)

[實作筆記] Stryker 升級與 CI 調整

前情提要

最近換了工作,一頭栽進了前端的領域,
生活又開始忙錄了起來,過年檢視了一下自已的 Side Project ,
意外發生 CI 竟然壞了一陣子,
這是我使用的變異測試工具 Stryker 從原本的 0.17 升級到了 1.3.1 版本
這是一個蠻大幅度的升版,所以設定上相當的不一樣。

我的 OS 是 macOS Monterey 12.1
安裝了 .NET 6.0

Local 環境

首先全域安裝 dotnet-stryker

dotnet tool install -g dotnet-stryker

接下安裝 tool-manifest

dotnet new tool-manifest

這時在你專案底下應該會建立一個 .config 資料夾,
內含 dotnet-tools.json 檔,內容如下

1
2
3
4
5
6
7
8
9
10
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-stryker": {
"version": "1.3.1",
"commands": ["dotnet-stryker"]
}
}
}

下一步,在專案資料下執行以下命令

dotnet tool install dotnet-stryker

建立 stryker-config.json 檔案
最後設定你的 config 檔 stryker-config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"stryker-config": {
"project": "Marsen.NetCore.Dojo.csproj",
"test-projects": [
"./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj",
"./test/Marsen.NetCore.Dojo.Integration.Tests/Marsen.NetCore.Dojo.Integration.Tests.csproj"
],
"reporters": ["progress", "json", "dashboard"],
"project-info": {
"name": "github.com/marsen/Marsen.NetCore.Dojo",
"version": "main"
}
}
}

與之前最大的不同是,大部份的參數你都可以設定在 stryker-config 之中,
而不用在執行命令中傳入,不僅可以為其提供版本控制,更能有序的組織你的設定檔,
使其更好閱讀。
如果你的 reporters 中有設定 dashboard 需要額外加上 --dashboard-api-key
ex:

dotnet stryker –dashboard-api-key=$STRYKER_DASHBOARD_API_KEY

參考

(fin)

[實作筆記] React Practice (一) Firebase 與 React Login

前情提要

本文不會介紹 FirebaseReact.
依照這篇文章完成後,你會

  • 透過 CRA 建立一個專案
  • 透過 MUI 建立符合 Material Design 的登入畫面
  • 建立 Firsbase 專案並且透過 Firsbase 完成登入登出的功能
  • 建立登入後才能瀏覽的頁面,並且實作登入/登出功能,登出後將無法瀏覽

Firebase 設定

建立 Firebase 專案

這裡的操作非常簡單,可以直接參考下面的影片。

詳細的步驟如下

  1. 進入 firebase console
  2. 選擇建立專案(Add Project),並且為你的專案取一個名字
  3. 進入剛剛建立的專案後,選擇建立應用程式(Add App),
    在這裡我們選擇 Web 類型的應用程式
    建立完成後我們會得到一個範例檔如下, 部份機敏資料先上遮罩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "A****************",
authDomain: "******.firebaseapp.com",
projectId: "******",
storageBucket: "******.appspot.com",
messagingSenderId: "******",
appId: "1:******:web:******",
measurementId: "G-*********",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

抽出環境變數

在 React 當中的機敏資料,建議的作法是使用環境變數,
我們可以建立一個.env.local提供給開發測試用,
而 Production 環境請直接設定在環境變數中, 相關資料不會進版控.
以下幾點注意事項

  • 必需使用 REACT_APP_* 開頭才可讓 REACT APP 使用 ex:REACT_APP_APIKEY
  • 一般的建議是全大寫
  • 環境變數的更新不適用 Hot Reload,請重啟環境

啟用 Firebase 認証(Authentication)

  1. 首先要建立 Sign-in method > 原生供應商 > 電子郵件/密碼
  2. 建立第一個使用者 Users > Add User

React

Create React Application

我們透過 TypeScript 建立 React 專案

1
npx create-react-app my-app --template typescript

我們可以試著啟動專案看一下,

1
cd my-app
1
npm i
1
npm start

安裝相關的套件

emotion 相關

1
npm i @emotion/react @emotion/styled

material 相關

1
npm i @mui/material @mui/icons-material

firebase 相關

1
npm i firebase react-firebase-hooks

react-router-dom 相關

1
npm i react-router-dom@6

建立登入頁面

登入頁面與路由

在 src 中建立 pages 資料夾,
之後我們再建立 Login.tsx 檔案如下

1
2
3
4
5
import React from "react";

export const Login = () => {
return <>Login Page</>;
};

接下來我們要透過 react-router-dom 來建立路由
開啟 index.tsx 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Login } from "./pages/Login";

ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="" element={<App />} />
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

重新啟動專案,在瀏覽器輸入不同的網址將會看到不同的畫面,如此我們完成了頁面的路由。

快速建立符合 MUI 的登入頁面

我們可以透過 MUI 提供的 Template 來建立 Login 頁面
實作細節如下:

  • 參考 Source Code 建立 src/components/SigninSide 組件
  • 修改 src/pages/Login.tsx 如下:
1
2
3
4
5
import SignInSide from "../components/SignInSide";

export const Login = () => {
return <SignInSide />;
};

重新瀏覽登入畫面,就可以看到一個美觀的登入頁 .

實作登入功能

首先請花點時間看一下 Firebase 帳號密碼登入的文件
接下來我們將依照文件的解釋修改我們的 SigninSide 組件。
打開 SigninSide 組件,找到handleSubmit 函數如下:

1
2
3
4
5
6
7
8
9
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
// eslint-disable-next-line no-console
console.log({
email: data.get("email"),
password: data.get("password"),
});
};

修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import { app } from "../firebase-config";
....
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const auth = getAuth(app);
signInWithEmailAndPassword(
auth,
data.get("email")!.toString(),
data.get("password")!.toString()
)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
console.log("user", userCredential.user);
// ...
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
console.log("error", error);
});
};

題外話,data.get(“email”)!.toString()
當中的 !. 運算子可以參考 TypeScript Non-null assertion operator 的說明

用之前在 firebase 建立的帳密登入,登入成功的話可以在 Console 看到類似以下的資訊

1
2
UserImpl {providerId: 'firebase', emailVerified: false, isAnonymous:
false, tenantId: null, providerData: Array(1), …}

實作登入後進頁面的跳轉

這裡要使用 react-dom-router 提供的 hook useNavigate,
注意 React Hook 的使用限制

CRA 建立專案時同時會幫我們安裝檢查用的 Lint 所以不用太擔心。
同時我們把 getAuth(app) 也提取到 SignInSide Component 之外

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
////getAuth 可以不用寫在 handleSubmit 中
const auth = getAuth(app);

export default function SignInSide() {
////遵循 React Hook 的規則在最上層呼叫 Hook
const navigate = useNavigate();//取得 navigate
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
signInWithEmailAndPassword(
auth,
data.get("email")!.toString(),
data.get("password")!.toString()
)
.then((userCredential) => {
// Signed in
// console.log("user", userCredential.user);
navigate("/");// navigate 到首頁

實作登出的功能

調整 App.tsx 如下,移除不相關的程式,建立一個 Button 並準備好 onClick 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Button } from "@mui/material";
import "./App.css";
function App() {
return (
<div className="App">
<Button variant="outlined" onClick={() => {}}>
LOGOUT
</Button>
</div>
);
}

export default App;

路由設定,只有登入者才能看的頁面

透過 getAuth().currentUser 取得目前的登入者資料, 如果沒有登入者轉導到 Login 頁面,

1
2
3
4
5
6
7
8
9
10
11
12
13
import { getAuth } from "firebase/auth";
....
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route
path=""
element={getAuth().currentUser ? <App /> : <Navigate to="/login" /> }
/>
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>

不過這樣的作法會有問題,
getAuth().currentUser 是非同步取得的資訊,
所以你很有可能都會拿到 null 值,
在官方的建議作法是透過 onAuthStateChanged 註冊 Observer, index.tsx 會類似下面這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const auth = getAuth();

onAuthStateChanged(auth, (user) => {
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="" element={user ? <App /> : <Navigate to="login" />} />
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
});

Todo

  • Firebase Email link(passwordless sign-in)
  • Firebase Host Project
  • PageNotFound
  • 管理 Routes
  • more…

(つづく)